diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx index 0ae340bd0..66b380f1e 100644 --- a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx @@ -1,17 +1,22 @@ "use client"; import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { useReducedMotion } from "motion/react"; import Link from "next/link"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { AccountFilter } from "@/components/analytics/account-filter"; import { AccountSeriesChartCard } from "@/components/analytics/account-series-chart-card"; +import { ConnectAccountsButtons } from "@/components/analytics/connect-accounts-buttons"; import { FollowersCard } from "@/components/analytics/followers-card"; +import { ImpressionsShareCard } from "@/components/analytics/impressions-share-card"; +import { LeaderboardCard } from "@/components/analytics/leaderboard-card"; import { PostingPerformanceCard } from "@/components/analytics/posting-performance-card"; import { SummaryStats } from "@/components/analytics/summary-stats"; import { TopPostsCard } from "@/components/analytics/top-posts-card"; import { EmptyState } from "@/components/empty-state"; +import { InstrumentGrid } from "@/components/instrument/instrument-grid"; +import { InstrumentReveal } from "@/components/instrument/instrument-reveal"; import { PageContainer } from "@/components/layout/container"; -import { SectionHeader } from "@/components/layout/section-header"; import { useOrganizationsContext } from "@/components/providers/organization-provider"; import { ACCOUNT_SERIES_COLORS, @@ -25,6 +30,7 @@ import { useSocialOverview, useTopPosts, } from "@/lib/hooks/use-social-analytics"; +import { cn } from "@/lib/utils"; import type { TimelineMarker } from "@/types/analytics"; import { accountSeriesKey, @@ -33,8 +39,33 @@ import { buildTimelineDays, markerIndexForDate, } from "@/utils/analytics-charts"; +import { formatSyncClock } from "@/utils/instrument"; import { AnalyticsPageSkeleton } from "./skeleton"; +/* ───────────────────────────────────────────────────────── + * ANIMATION STORYBOARD — Analytics instrument panel + * + * Read top-to-bottom. Each `at` value is ms after data mount. + * + * 0ms header + account switches render statically + * 60ms master readout rail powers on, + * sync dot starts pulsing + * 150ms modules materialize over the grid substrate + * (staggered 45ms in reading order) + * + * Reduced motion: everything appears at once, no offsets. + * ───────────────────────────────────────────────────────── */ + +const TIMING = { + readoutRail: 60, // master readout rail powers on + modules: 150, // grid modules start staggering in +}; + +const STAGE = { + rail: 1, // readout rail visible + modules: 2, // grid modules visible +}; + interface PageClientProps { organizationSlug: string; } @@ -48,8 +79,11 @@ export default function PageClient({ organizationSlug }: PageClientProps) { : orgFromList; const organizationId = organization?.id ?? ""; - const { data: overview, isPending: isOverviewPending } = - useSocialOverview(organizationId); + const { + data: overview, + isPending: isOverviewPending, + dataUpdatedAt, + } = useSocialOverview(organizationId); const { data: engagement } = useEngagementTimeseries(organizationId); const { data: followerGrowth } = useFollowerGrowth(organizationId); const { data: topPosts } = useTopPosts(organizationId); @@ -59,6 +93,29 @@ export default function PageClient({ organizationSlug }: PageClientProps) { const [hoverIndex, setHoverIndex] = useState(null); const [hiddenKeys, setHiddenKeys] = useState>(new Set()); + const reduceMotion = useReducedMotion(); + const [stage, setStage] = useState(0); + const ready = !isOverviewPending; + + useEffect(() => { + if (!ready) { + setStage(0); + return; + } + if (reduceMotion) { + setStage(STAGE.modules); + return; + } + const timers: ReturnType[] = []; + timers.push(setTimeout(() => setStage(STAGE.rail), TIMING.readoutRail)); + timers.push(setTimeout(() => setStage(STAGE.modules), TIMING.modules)); + return () => { + for (const timer of timers) { + clearTimeout(timer); + } + }; + }, [ready, reduceMotion]); + const accounts = useMemo( () => overview?.accounts ?? [], [overview?.accounts] @@ -175,10 +232,12 @@ export default function PageClient({ organizationSlug }: PageClientProps) {
-

Analytics

-

- Performance of your connected X and LinkedIn accounts +

+ X + LinkedIn instrument panel

+

+ Nothing to report yet. +

-
+
-
-

Analytics

-

- {accounts.length} {accounts.length === 1 ? "account" : "accounts"}{" "} - connected · hover any chart to compare the same day everywhere -

+
+
+

+ Analytics +

+

+ + {accounts.length}{" "} + {accounts.length === 1 ? "account" : "accounts"} · X + + LinkedIn · {ANALYTICS_TIMESERIES_DAYS}D window · Sync{" "} + {formatSyncClock(dataUpdatedAt || null)} + +

+
+
{overview?.configured === false && ( -

+

Analytics ingestion is not configured yet. Connected accounts are shown, but stats will appear once the analytics backend is set up.

)} - + = STAGE.rail}> + + -
- -
+ + = STAGE.modules} + className="lg:col-span-8" + order={0} + > + + = STAGE.modules} + className="lg:col-span-4" + order={1} + > + accountConfig[key]?.color ?? "blue"} + hiddenKeys={hiddenKeys} + points={followerGrowth?.points ?? []} + /> + + = STAGE.modules} + className="lg:col-span-8" + order={2} + > + + + = STAGE.modules} + className="lg:col-span-4" + order={3} + > + + + = STAGE.modules} + className="lg:col-span-6" + order={4} + > + + = STAGE.modules} + className="lg:col-span-6" + order={5} + > - accountConfig[key]?.color ?? "blue"} - hiddenKeys={hiddenKeys} - points={followerGrowth?.points ?? []} - /> -
-
- -
- -
+ + = STAGE.modules} + className="lg:col-span-6" + order={6} + > + + = STAGE.modules} + className="lg:col-span-6" + order={7} + > -
-
+ +
); diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx index c60f8ca10..9c6972963 100644 --- a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx @@ -4,35 +4,34 @@ import { Skeleton } from "@notra/ui/components/ui/skeleton"; import { useId } from "react"; import { PageContainer } from "@/components/layout/container"; -const ACCOUNT_CARD_COUNT = 2; -const CHART_CARD_COUNT = 2; +const RAIL_TILE_COUNT = 4; +const MODULE_COUNT = 4; export function AnalyticsPageSkeleton() { const id = useId(); return ( -
+
- - + +
-
- {Array.from({ length: ACCOUNT_CARD_COUNT }).map((_, index) => ( +
+ {Array.from({ length: RAIL_TILE_COUNT }).map((_, index) => ( ))}
-
- {Array.from({ length: CHART_CARD_COUNT }).map((_, index) => ( +
+ {Array.from({ length: MODULE_COUNT }).map((_, index) => ( ))}
-
); diff --git a/apps/dashboard/src/components/analytics/account-filter.tsx b/apps/dashboard/src/components/analytics/account-filter.tsx index 5bd48ad42..e1d9c9ac2 100644 --- a/apps/dashboard/src/components/analytics/account-filter.tsx +++ b/apps/dashboard/src/components/analytics/account-filter.tsx @@ -34,9 +34,9 @@ export function AccountFilter({ + +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/followers-card.tsx b/apps/dashboard/src/components/analytics/followers-card.tsx index 484ebbd1c..2bf0738ac 100644 --- a/apps/dashboard/src/components/analytics/followers-card.tsx +++ b/apps/dashboard/src/components/analytics/followers-card.tsx @@ -8,12 +8,9 @@ import { AvatarImage, } from "@notra/ui/components/ui/avatar"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@notra/ui/components/ui/card"; + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; import { cn } from "@/lib/utils"; import type { FollowerGrowthPoint, @@ -49,17 +46,23 @@ function DeltaBadge({ series }: { series: number[] }) { const last = series.at(-1); if (first === undefined || last === undefined || series.length < 2) { return ( - tracking started + + tracking started + ); } const delta = last - first; if (delta === 0) { - return ±0; + return ( + + ±0 + + ); } return ( 0 ? "text-green-500" : "text-red-500" )} > @@ -82,70 +85,63 @@ export function FollowersCard({ ); return ( - - - Followers - - Live counts with change since tracking began; X exposes no earlier - history - - - - {visible.length === 0 ? ( -

- No accounts selected -

- ) : ( -
- {visible.map((account) => { - const key = accountSeriesKey( - account.provider, - account.providerAccountId - ); - const series = seriesFor( - points, - account.provider, - account.providerAccountId - ); - return ( -
- - {account.profileImageUrl && ( - - )} - - {account.username.slice(0, 2).toUpperCase()} - - -
-

- @{account.username} -

-
- - {formatMetric(account.followersCount)} - - -
-
-
- {series.length >= 2 && ( - - )} + + {visible.length === 0 ? ( + + ) : ( +
+ {visible.map((account) => { + const key = accountSeriesKey( + account.provider, + account.providerAccountId + ); + const series = seriesFor( + points, + account.provider, + account.providerAccountId + ); + return ( +
+ + {account.profileImageUrl && ( + + )} + + {account.username.slice(0, 2).toUpperCase()} + + +
+

+ @{account.username} +

+
+ + {formatMetric(account.followersCount)} + +
- ); - })} -
- )} - - +
+ {series.length >= 2 && ( + + )} +
+
+ ); + })} +
+ )} + ); } diff --git a/apps/dashboard/src/components/analytics/impressions-share-card.tsx b/apps/dashboard/src/components/analytics/impressions-share-card.tsx new file mode 100644 index 000000000..403a87e71 --- /dev/null +++ b/apps/dashboard/src/components/analytics/impressions-share-card.tsx @@ -0,0 +1,123 @@ +"use client"; + +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { PALETTE, rgb } from "@notra/ui/components/dither-kit/palette"; +import { Pie } from "@notra/ui/components/dither-kit/pie"; +import { PieChart } from "@notra/ui/components/dither-kit/pie-chart"; +import { Tooltip } from "@notra/ui/components/dither-kit/tooltip"; +import { useMemo } from "react"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { ACCOUNT_SERIES_COLORS } from "@/constants/analytics"; +import { useLeaderboard } from "@/lib/hooks/use-social-analytics"; + +interface ImpressionsShareCardProps { + organizationId: string; +} + +interface ShareRow { + account: string; + impressions: number; +} + +const DONUT_INNER_RADIUS = 0.55; +const WINDOW_DAYS = 30; +const PERCENT = 100; + +export function ImpressionsShareCard({ + organizationId, +}: ImpressionsShareCardProps) { + const { data } = useLeaderboard(organizationId, WINDOW_DAYS); + + const { rows, config, total, caption } = useMemo(() => { + const shareRows: ShareRow[] = (data?.entries ?? []) + .filter((entry) => (entry.impressions ?? 0) > 0) + .map((entry) => ({ + account: `@${entry.username}`, + impressions: entry.impressions ?? 0, + })); + const shareConfig: ChartConfig = {}; + shareRows.forEach((row, index) => { + shareConfig[row.account] = { + label: row.account, + color: + ACCOUNT_SERIES_COLORS[index % ACCOUNT_SERIES_COLORS.length] ?? "blue", + }; + }); + const shareTotal = shareRows.reduce((sum, row) => sum + row.impressions, 0); + const top = shareRows.reduce( + (best, row) => + best === null || row.impressions > best.impressions ? row : best, + null + ); + return { + rows: shareRows, + config: shareConfig, + total: shareTotal, + caption: + top && shareTotal > 0 + ? `${top.account} · ${Math.round((top.impressions / shareTotal) * PERCENT)}% of impressions` + : null, + }; + }, [data?.entries]); + + return ( + + {rows.length === 0 ? ( + + ) : ( +
+
+ + + + +
+ {rows.map((row) => ( +
+ + + {row.account} + + + {total > 0 + ? `${Math.round((row.impressions / total) * PERCENT)}%` + : "0%"} + +
+ ))} +
+
+ {caption && ( +

+ {caption} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/leaderboard-card.tsx b/apps/dashboard/src/components/analytics/leaderboard-card.tsx new file mode 100644 index 000000000..866ec2a97 --- /dev/null +++ b/apps/dashboard/src/components/analytics/leaderboard-card.tsx @@ -0,0 +1,261 @@ +"use client"; + +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@notra/ui/components/ui/avatar"; +import { Button } from "@notra/ui/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@notra/ui/components/ui/select"; +import { useState } from "react"; +import { XVerificationBadge } from "@/components/icons/x-verification-badge"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { LEADERBOARD_WINDOWS } from "@/constants/analytics"; + +import { + useLeaderboard, + useUntrackAccount, +} from "@/lib/hooks/use-social-analytics"; +import { cn } from "@/lib/utils"; +import type { + LeaderboardEntry, + LeaderboardWindow, + SocialOverviewAccount, +} from "@/types/analytics"; +import { formatMetric } from "@/utils/analytics-charts"; +import { isSquareTwitterAvatar } from "@/utils/twitter"; + +interface LeaderboardCardProps { + organizationId: string; + accountDetails: SocialOverviewAccount[]; +} + +interface DetailMetric { + label: string; + value: string; +} + +function detailMetrics(account: SocialOverviewAccount): DetailMetric[] { + const interactions = + (account.likes ?? 0) + (account.replies ?? 0) + (account.reposts ?? 0); + const engagementRate = + account.impressions && account.impressions > 0 + ? `${((interactions / account.impressions) * 100).toFixed(1)}%` + : "N/A"; + return [ + { label: "Followers", value: formatMetric(account.followersCount) }, + { label: "Impressions", value: formatMetric(account.impressions) }, + { label: "Likes", value: formatMetric(account.likes) }, + { label: "Replies", value: formatMetric(account.replies) }, + { label: "Reposts", value: formatMetric(account.reposts) }, + { label: "Quotes", value: formatMetric(account.quotes) }, + { label: "Bookmarks", value: formatMetric(account.bookmarks) }, + { label: "Eng. rate", value: engagementRate }, + ]; +} + +function LeaderboardRow({ + entry, + organizationId, + detail, + expanded, + onToggleExpand, +}: { + entry: LeaderboardEntry; + organizationId: string; + detail: SocialOverviewAccount | null; + expanded: boolean; + onToggleExpand: () => void; +}) { + const untrack = useUntrackAccount(organizationId); + + return ( +
+
+ + + {entry.trackedAccountId && ( + + )} + +
+ {expanded && detail && ( +
+ {detailMetrics(detail).map((metric) => ( +
+
+ {metric.label} +
+
{metric.value}
+
+ ))} +
+ )} + {expanded && !detail && ( +

+ Lifetime stats appear after this account's first sync +

+ )} +
+ ); +} + +export function LeaderboardCard({ + organizationId, + accountDetails, +}: LeaderboardCardProps) { + const [days, setDays] = useState(7); + const [expandedKey, setExpandedKey] = useState(null); + const detailsByUsername = new Map( + accountDetails.map((account) => [account.username.toLowerCase(), account]) + ); + const { data } = useLeaderboard(organizationId, days); + + const entries = data?.entries ?? []; + + return ( + { + const parsed = LEADERBOARD_WINDOWS.find( + (window) => String(window) === value + ); + if (parsed) { + setDays(parsed); + } + }} + value={String(days)} + > + + {`${days}D`} + + + {LEADERBOARD_WINDOWS.map((window) => ( + + Last {window}d + + ))} + + + } + eyebrow="Leaderboard" + readout="ranked by interactions" + > +
+ Rk + + Account + Interact + Impress + Posts + +
+ {entries.length === 0 ? ( + + ) : ( +
+ {entries.map((entry) => ( + + setExpandedKey((previous) => + previous === entry.key ? null : entry.key + ) + } + organizationId={organizationId} + /> + ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/posting-performance-card.tsx b/apps/dashboard/src/components/analytics/posting-performance-card.tsx index 67b43f658..2b0385039 100644 --- a/apps/dashboard/src/components/analytics/posting-performance-card.tsx +++ b/apps/dashboard/src/components/analytics/posting-performance-card.tsx @@ -7,15 +7,12 @@ import { Grid } from "@notra/ui/components/dither-kit/grid"; import { Tooltip } from "@notra/ui/components/dither-kit/tooltip"; import { XAxis } from "@notra/ui/components/dither-kit/x-axis"; import { YAxis } from "@notra/ui/components/dither-kit/y-axis"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@notra/ui/components/ui/card"; import { useState } from "react"; import { ChartSeriesLegend } from "@/components/analytics/chart-legend"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; import type { PostingPerformanceChartRow } from "@/types/analytics"; interface PostingPerformanceCardProps { @@ -47,36 +44,30 @@ export function PostingPerformanceCard({ rows }: PostingPerformanceCardProps) { }; return ( - - - Best days to post - - Average engagement per post and post volume by weekday, last 90 days - - - - {hasData && visibleKeys.length > 0 ? ( - - - - - {visibleKeys.map((key) => ( - - ))} - - - ) : ( -

- No posting data yet -

- )} - + {hasData && visibleKeys.length > 0 ? ( + + + + + {visibleKeys.map((key) => ( + + ))} + + + ) : ( + -
-
+ )} + + ); } diff --git a/apps/dashboard/src/components/analytics/summary-stats.tsx b/apps/dashboard/src/components/analytics/summary-stats.tsx index b9ff4c197..7b320e795 100644 --- a/apps/dashboard/src/components/analytics/summary-stats.tsx +++ b/apps/dashboard/src/components/analytics/summary-stats.tsx @@ -1,77 +1,69 @@ "use client"; -import { Card, CardContent } from "@notra/ui/components/ui/card"; import { useMemo } from "react"; -import type { - EngagementTimeseriesPoint, - SocialOverviewAccount, -} from "@/types/analytics"; -import { formatMetric, sumMetric } from "@/utils/analytics-charts"; - -interface SummaryStatsProps { - accounts: SocialOverviewAccount[]; - points: EngagementTimeseriesPoint[]; -} - -interface StatTile { - label: string; - value: string; - hint: string; -} - -const PERCENT = 100; +import { InstrumentGrid } from "@/components/instrument/instrument-grid"; +import { cn } from "@/lib/utils"; +import type { AnalyticsStatTile, SummaryStatsProps } from "@/types/analytics"; +import { + buildAnalyticsHeroSummary, + formatMetric, +} from "@/utils/analytics-charts"; export function SummaryStats({ accounts, points }: SummaryStatsProps) { - const tiles = useMemo(() => { - const followers = sumMetric(accounts, (account) => account.followersCount); - let impressions = 0; - let interactions = 0; - let posts = 0; - for (const point of points) { - impressions += point.impressions ?? 0; - interactions += - (point.likes ?? 0) + (point.replies ?? 0) + (point.reposts ?? 0); - posts += point.posts; - } - const engagementRate = - impressions > 0 ? (interactions / impressions) * PERCENT : null; + const tiles = useMemo(() => { + const summary = buildAnalyticsHeroSummary(accounts, points); return [ + { + label: "Engagement rate", + value: + summary.engagementRate === null + ? "N/A" + : `${summary.engagementRate.toFixed(1)}%`, + hint: `${formatMetric(summary.interactions)} interactions / ${formatMetric(summary.impressions)} impressions`, + accent: true, + }, { label: "Followers", - value: formatMetric(followers), + value: formatMetric(summary.followers), hint: "across connected accounts", + accent: false, }, { label: "Impressions", - value: formatMetric(impressions), + value: formatMetric(summary.impressions), hint: "posts from the last 30 days", + accent: false, }, { label: "Interactions", - value: formatMetric(interactions), - hint: `${posts} posts, last 30 days`, - }, - { - label: "Engagement rate", - value: - engagementRate === null ? "N/A" : `${engagementRate.toFixed(1)}%`, - hint: "interactions per impression", + value: formatMetric(summary.interactions), + hint: `${summary.posts} posts, last 30 days`, + accent: false, }, ]; }, [accounts, points]); return ( -
+ {tiles.map((tile) => ( - - -

{tile.label}

-

{tile.value}

-

{tile.hint}

-
-
+
+

+ {tile.label} +

+

+ {tile.value} +

+

+ {tile.hint} +

+
))} -
+ ); } diff --git a/apps/dashboard/src/components/analytics/top-posts-card.tsx b/apps/dashboard/src/components/analytics/top-posts-card.tsx index 618cb0537..2f87c7d7f 100644 --- a/apps/dashboard/src/components/analytics/top-posts-card.tsx +++ b/apps/dashboard/src/components/analytics/top-posts-card.tsx @@ -8,12 +8,9 @@ import { AvatarImage, } from "@notra/ui/components/ui/avatar"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@notra/ui/components/ui/card"; + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; import { TOP_POST_CONTENT_PREVIEW_LENGTH } from "@/constants/analytics"; import type { TopPostItem } from "@/types/analytics"; import { formatDayLabel, formatMetric } from "@/utils/analytics-charts"; @@ -33,7 +30,7 @@ function previewContent(content: string): string { function PostAvatar({ post }: { post: TopPostItem }) { const name = post.username ?? post.providerAccountId; return ( - + {post.profileImageUrl && ( )} @@ -49,7 +46,7 @@ function PostRow({ post }: { post: TopPostItem }) {
-

+

{previewContent(post.content)}

-

+

{formatMetric(post.likes)} likes · {formatMetric(post.replies)}{" "} replies · {formatMetric(post.reposts)} reposts {post.impressions !== null && ` · ${formatMetric(post.impressions)} impressions`}

- + {formatMetric(post.engagement)}
@@ -77,7 +74,7 @@ function PostRow({ post }: { post: TopPostItem }) { if (post.url) { return ( ); } - return
{body}
; + return
{body}
; } export function TopPostsCard({ posts }: TopPostsCardProps) { return ( - - - Top posts - - Ranked by likes, replies, and reposts from the latest sync - - - - {posts.length === 0 ? ( -

- No tracked posts yet -

- ) : ( -
- {posts.map((post) => ( - - ))} -
- )} -
-
+ + {posts.length === 0 ? ( + + ) : ( +
+ {posts.map((post) => ( + + ))} +
+ )} +
); } diff --git a/apps/dashboard/src/components/instrument/instrument-grid.tsx b/apps/dashboard/src/components/instrument/instrument-grid.tsx new file mode 100644 index 000000000..579ca94c3 --- /dev/null +++ b/apps/dashboard/src/components/instrument/instrument-grid.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; +import type { InstrumentGridProps } from "@/types/instrument"; + +export function InstrumentGrid({ children, className }: InstrumentGridProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/dashboard/src/components/instrument/instrument-module.tsx b/apps/dashboard/src/components/instrument/instrument-module.tsx new file mode 100644 index 000000000..b13b7c6b4 --- /dev/null +++ b/apps/dashboard/src/components/instrument/instrument-module.tsx @@ -0,0 +1,54 @@ +import { DitherAvatar } from "@notra/ui/components/dither-kit/avatar"; +import { cn } from "@/lib/utils"; +import type { + InstrumentEmptyProps, + InstrumentModuleProps, +} from "@/types/instrument"; + +export function InstrumentModule({ + eyebrow, + readout, + action, + children, + className, + bodyClassName, +}: InstrumentModuleProps) { + return ( +
+
+

+ {eyebrow} +

+
+ {readout && ( + + {readout} + + )} + {action} +
+
+
{children}
+
+ ); +} + +export function InstrumentEmpty({ + seed, + message, + className, +}: InstrumentEmptyProps) { + return ( +
+ +

+ {message} +

+
+ ); +} diff --git a/apps/dashboard/src/components/instrument/instrument-reveal.tsx b/apps/dashboard/src/components/instrument/instrument-reveal.tsx new file mode 100644 index 000000000..8f6e183b7 --- /dev/null +++ b/apps/dashboard/src/components/instrument/instrument-reveal.tsx @@ -0,0 +1,51 @@ +"use client"; + +import type { Transition } from "motion/react"; +import { motion, useReducedMotion } from "motion/react"; +import { cn } from "@/lib/utils"; +import type { InstrumentRevealProps } from "@/types/instrument"; + +/* ───────────────────────────────────────────────────────── + * ANIMATION STORYBOARD — module reveal + * + * Each module powers on once its page stage goes active: + * + * 0ms dark cell (opacity 0, y +0.375rem) + * +n opacity 0 → 1, y +0.375rem → 0 (45ms per order step) + * + * Reduced motion: modules appear instantly, no offset. + * ───────────────────────────────────────────────────────── */ + +const REVEAL = { + offsetY: 6, // px each module rises while powering on + stagger: 0.045, // seconds between neighboring modules + spring: { type: "spring", stiffness: 380, damping: 34 }, +} satisfies { offsetY: number; stagger: number; spring: Transition }; + +const INSTANT: Transition = { duration: 0 }; + +export function InstrumentReveal({ + active, + order = 0, + children, + className, +}: InstrumentRevealProps) { + const reduceMotion = useReducedMotion(); + const transition: Transition = reduceMotion + ? INSTANT + : { ...REVEAL.spring, delay: order * REVEAL.stagger }; + + return ( + + {children} + + ); +} diff --git a/apps/dashboard/src/constants/analytics.ts b/apps/dashboard/src/constants/analytics.ts index aef7bf976..b42ca2707 100644 --- a/apps/dashboard/src/constants/analytics.ts +++ b/apps/dashboard/src/constants/analytics.ts @@ -18,3 +18,5 @@ export const ACCOUNT_SERIES_COLORS: DitherColor[] = [ export const ANALYTICS_TIMESERIES_DAYS = 30; export const ANALYTICS_TOP_POSTS_LIMIT = 8; export const TOP_POST_CONTENT_PREVIEW_LENGTH = 96; + +export const LEADERBOARD_WINDOWS = [7, 30] as const; diff --git a/apps/dashboard/src/lib/analytics/leaderboard.ts b/apps/dashboard/src/lib/analytics/leaderboard.ts new file mode 100644 index 000000000..426ce9d77 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/leaderboard.ts @@ -0,0 +1,98 @@ +import type { + LeaderboardAccount, + LeaderboardEntry, + LeaderboardWindowTotals, +} from "@/types/analytics"; + +function leaderboardAccountKey( + provider: string, + providerAccountId: string +): string { + return `${provider}:${providerAccountId}`; +} + +interface RankableTotals { + key: string; + interactions: number; + impressions: number; +} + +function rankByInteractions(items: RankableTotals[]): Map { + const sorted = [...items].sort((left, right) => { + if (right.interactions !== left.interactions) { + return right.interactions - left.interactions; + } + return right.impressions - left.impressions; + }); + return new Map(sorted.map((item, index) => [item.key, index + 1])); +} + +export function buildLeaderboardEntries( + accounts: LeaderboardAccount[], + totals: LeaderboardWindowTotals[] +): LeaderboardEntry[] { + const totalsByKey = new Map( + totals.map((row) => [ + leaderboardAccountKey(row.provider, row.providerAccountId), + row, + ]) + ); + + const current: RankableTotals[] = []; + const previous: RankableTotals[] = []; + + for (const account of accounts) { + const key = leaderboardAccountKey( + account.provider, + account.providerAccountId + ); + const row = totalsByKey.get(key); + current.push({ + key, + interactions: row?.interactions ?? 0, + impressions: row?.impressions ?? 0, + }); + const hasPreviousData = + (row?.previousPosts ?? 0) > 0 || (row?.previousInteractions ?? 0) > 0; + if (hasPreviousData && row) { + previous.push({ + key, + interactions: row.previousInteractions, + impressions: row.previousImpressions, + }); + } + } + + const currentRanks = rankByInteractions(current); + const previousRanks = rankByInteractions(previous); + + const entries = accounts.map((account) => { + const key = leaderboardAccountKey( + account.provider, + account.providerAccountId + ); + const row = totalsByKey.get(key); + const rank = currentRanks.get(key) ?? accounts.length; + const previousRank = previousRanks.get(key) ?? null; + return { + key, + provider: account.provider, + providerAccountId: account.providerAccountId, + username: account.username, + displayName: account.displayName, + profileImageUrl: account.profileImageUrl, + verified: account.verified, + verifiedType: account.verifiedType, + isConnected: account.isConnected, + trackedAccountId: account.trackedAccountId, + rank, + previousRank, + rankChange: previousRank === null ? null : previousRank - rank, + interactions: row?.interactions ?? 0, + impressions: row?.impressions ?? null, + posts: row?.posts ?? 0, + }; + }); + + return entries.sort((left, right) => left.rank - right.rank); +} diff --git a/apps/dashboard/src/lib/analytics/tracked-accounts.ts b/apps/dashboard/src/lib/analytics/tracked-accounts.ts new file mode 100644 index 000000000..e54ec8687 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/tracked-accounts.ts @@ -0,0 +1,48 @@ +import { normalizeTwitterProfileImageUrl } from "@/constants/twitter"; +import type { + ResolvedTwitterAccount, + TwitterUserByUsernameResponse, +} from "@/types/analytics"; +import { twitterAppFetch } from "@/utils/twitter-fetcher"; + +const TWITTER_RESOLVE_USER_FIELDS = + "name,profile_image_url,public_metrics,verified,verified_type"; +const LEADING_AT_REGEX = /^@/; + +export async function resolveTwitterAccount( + username: string +): Promise { + const handle = username.trim().replace(LEADING_AT_REGEX, ""); + if (handle.length === 0) { + return null; + } + + const params = new URLSearchParams({ + "user.fields": TWITTER_RESOLVE_USER_FIELDS, + }); + const response = await twitterAppFetch( + `https://api.x.com/2/users/by/username/${encodeURIComponent(handle)}?${params.toString()}` + ); + if (!response.ok) { + return null; + } + + const json: TwitterUserByUsernameResponse = await response.json(); + const user = json.data; + if (!user?.id) { + return null; + } + + const verifiedType = user.verified_type ?? "none"; + return { + providerAccountId: user.id, + username: user.username, + displayName: user.name ?? null, + profileImageUrl: user.profile_image_url + ? normalizeTwitterProfileImageUrl(user.profile_image_url) + : null, + verified: user.verified === true || verifiedType !== "none", + verifiedType, + followersCount: user.public_metrics?.followers_count ?? null, + }; +} diff --git a/apps/dashboard/src/lib/hooks/use-social-analytics.ts b/apps/dashboard/src/lib/hooks/use-social-analytics.ts index 2ecc60b08..4390d2a5b 100644 --- a/apps/dashboard/src/lib/hooks/use-social-analytics.ts +++ b/apps/dashboard/src/lib/hooks/use-social-analytics.ts @@ -1,9 +1,12 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import type { EngagementTimeseriesResponse, FollowerGrowthResponse, + LeaderboardResponse, + LeaderboardWindow, NotraAdoptionResponse, PostingPerformanceResponse, SocialOverviewResponse, @@ -66,6 +69,42 @@ export function usePostingPerformance(organizationId: string, days?: number) { }); } +export function useLeaderboard( + organizationId: string, + days: LeaderboardWindow +) { + return useQuery({ + ...dashboardOrpc.analytics.leaderboard.queryOptions({ + input: { organizationId, days }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load leaderboard" }, + }); +} + +export function useUntrackAccount(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (trackedAccountId: string) => + dashboardOrpc.analytics.untrackAccount.call({ + organizationId, + trackedAccountId, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: dashboardOrpc.analytics.leaderboard.key(), + }); + }, + onError: (error) => { + toast.error( + error instanceof Error && error.message + ? error.message + : "Failed to stop tracking account" + ); + }, + }); +} + export function useNotraAdoption(organizationId: string) { return useQuery({ ...dashboardOrpc.analytics.adoption.queryOptions({ diff --git a/apps/dashboard/src/lib/orpc/routers/analytics.ts b/apps/dashboard/src/lib/orpc/routers/analytics.ts index cad73c845..706112242 100644 --- a/apps/dashboard/src/lib/orpc/routers/analytics.ts +++ b/apps/dashboard/src/lib/orpc/routers/analytics.ts @@ -1,5 +1,10 @@ import { + ingestSocialAccountStats, + ingestSocialAccounts, + ingestSocialPostStats, + ingestSocialPosts, isTinybirdConfigured, + queryAccountLeaderboard, queryEngagementTimeseries, queryFollowerGrowth, queryNotraAdoption, @@ -7,29 +12,43 @@ import { querySocialOverview, queryTopPosts, } from "@notra/analytics/tinybird/client"; +import { purgeSocialAccountData } from "@notra/analytics/tinybird/purge"; import { db } from "@notra/db/drizzle"; import { connectedSocialAccounts, organizations, posts, + trackedSocialAccounts, } from "@notra/db/schema"; import { and, asc, eq } from "drizzle-orm"; +import { buildLeaderboardEntries } from "@/lib/analytics/leaderboard"; +import { buildAccountRow } from "@/lib/analytics/rows"; +import { resolveTwitterAccount } from "@/lib/analytics/tracked-accounts"; +import { collectTwitterRows } from "@/lib/analytics/twitter-sync"; import { assertOrganizationAccess } from "@/lib/auth/organization"; import { authorizedProcedure } from "@/lib/orpc/base"; import { analyticsOrganizationInputSchema, analyticsTimeseriesInputSchema, analyticsTopPostsInputSchema, + leaderboardInputSchema, + trackAccountInputSchema, + untrackAccountInputSchema, } from "@/schemas/analytics"; import type { EngagementTimeseriesResponse, + TrackAccountPreviewResponse, FollowerGrowthResponse, + LeaderboardAccount, + LeaderboardResponse, NotraAdoptionResponse, PostingPerformanceResponse, SocialOverviewAccount, SocialOverviewResponse, + SyncableSocialAccount, TopPostsResponse, } from "@/types/analytics"; +import { badRequest, notFound } from "../utils/errors"; function toNullableNumber(value: number | bigint | null): number | null { if (value === null) { @@ -38,6 +57,17 @@ function toNullableNumber(value: number | bigint | null): number | null { return Number(value); } +async function syncTrackedAccountNow( + account: SyncableSocialAccount +): Promise { + const capturedAt = new Date(); + await ingestSocialAccounts([buildAccountRow(account, capturedAt)]); + const rows = await collectTwitterRows([account], capturedAt); + await ingestSocialAccountStats(rows.accountStats); + await ingestSocialPosts(rows.posts); + await ingestSocialPostStats(rows.postStats); +} + export const analyticsRouter = { overview: authorizedProcedure .input(analyticsOrganizationInputSchema) @@ -150,7 +180,7 @@ export const analyticsRouter = { user: context.user, }); - const [result, accounts] = await Promise.all([ + const [result, accounts, tracked] = await Promise.all([ queryTopPosts({ organization_id: input.organizationId, limit: input.limit, @@ -170,10 +200,19 @@ export const analyticsRouter = { input.organizationId ), }), + db.query.trackedSocialAccounts.findMany({ + columns: { + provider: true, + providerAccountId: true, + username: true, + profileImageUrl: true, + }, + where: eq(trackedSocialAccounts.organizationId, input.organizationId), + }), ]); const accountsByKey = new Map( - accounts.map((account) => [ + [...accounts, ...tracked].map((account) => [ `${account.provider}:${account.providerAccountId}`, account, ]) @@ -181,27 +220,31 @@ export const analyticsRouter = { return { configured: isTinybirdConfigured(), - posts: (result?.data ?? []).map((row) => { - const account = accountsByKey.get( - `${row.provider}:${row.provider_account_id}` - ); - return { - provider: row.provider, - platformPostId: row.platform_post_id, - providerAccountId: row.provider_account_id, - username: account?.username ?? null, - profileImageUrl: account?.profileImageUrl ?? null, - content: row.content, - url: row.url, - postedAt: row.posted_at, - impressions: toNullableNumber(row.impressions), - likes: toNullableNumber(row.likes), - replies: toNullableNumber(row.replies), - reposts: toNullableNumber(row.reposts), - bookmarks: toNullableNumber(row.bookmarks), - engagement: Number(row.engagement), - }; - }), + posts: (result?.data ?? []) + .filter((row) => + accountsByKey.has(`${row.provider}:${row.provider_account_id}`) + ) + .map((row) => { + const account = accountsByKey.get( + `${row.provider}:${row.provider_account_id}` + ); + return { + provider: row.provider, + platformPostId: row.platform_post_id, + providerAccountId: row.provider_account_id, + username: account?.username ?? null, + profileImageUrl: account?.profileImageUrl ?? null, + content: row.content, + url: row.url, + postedAt: row.posted_at, + impressions: toNullableNumber(row.impressions), + likes: toNullableNumber(row.likes), + replies: toNullableNumber(row.replies), + reposts: toNullableNumber(row.reposts), + bookmarks: toNullableNumber(row.bookmarks), + engagement: Number(row.engagement), + }; + }), }; }), adoption: authorizedProcedure @@ -278,6 +321,248 @@ export const analyticsRouter = { }; } ), + leaderboard: authorizedProcedure + .input(leaderboardInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [connected, tracked, result] = await Promise.all([ + db.query.connectedSocialAccounts.findMany({ + columns: { + provider: true, + providerAccountId: true, + username: true, + displayName: true, + profileImageUrl: true, + verified: true, + verifiedType: true, + }, + where: eq( + connectedSocialAccounts.organizationId, + input.organizationId + ), + }), + db.query.trackedSocialAccounts.findMany({ + columns: { + id: true, + provider: true, + providerAccountId: true, + username: true, + displayName: true, + profileImageUrl: true, + verified: true, + verifiedType: true, + }, + where: eq(trackedSocialAccounts.organizationId, input.organizationId), + }), + queryAccountLeaderboard({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[Analytics] leaderboard query failed:", error); + return null; + }), + ]); + + const connectedKeys = new Set( + connected.flatMap((account) => [ + `${account.provider}:${account.providerAccountId}`, + `${account.provider}:@${account.username.toLowerCase()}`, + ]) + ); + + const accounts: LeaderboardAccount[] = [ + ...connected.map((account) => ({ + provider: account.provider, + providerAccountId: account.providerAccountId, + username: account.username, + displayName: account.displayName, + profileImageUrl: account.profileImageUrl, + verified: account.verified ?? false, + verifiedType: account.verifiedType, + isConnected: true, + trackedAccountId: null, + })), + ...tracked + .filter( + (account) => + !( + connectedKeys.has( + `${account.provider}:${account.providerAccountId}` + ) || + connectedKeys.has( + `${account.provider}:@${account.username.toLowerCase()}` + ) + ) + ) + .map((account) => ({ + provider: account.provider, + providerAccountId: account.providerAccountId, + username: account.username, + displayName: account.displayName, + profileImageUrl: account.profileImageUrl, + verified: account.verified, + verifiedType: account.verifiedType, + isConnected: false, + trackedAccountId: account.id, + })), + ]; + + const totals = (result?.data ?? []).map((row) => ({ + provider: row.provider, + providerAccountId: row.provider_account_id, + posts: Number(row.posts), + interactions: Number(row.interactions), + impressions: Number(row.impressions), + previousPosts: Number(row.prev_posts), + previousInteractions: Number(row.prev_interactions), + previousImpressions: Number(row.prev_impressions), + })); + + return { + configured: isTinybirdConfigured(), + days: input.days, + entries: buildLeaderboardEntries(accounts, totals), + }; + }), + previewTrackAccount: authorizedProcedure + .input(trackAccountInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const account = await resolveTwitterAccount(input.username).catch( + (error) => { + console.error("[Analytics] account preview failed:", error); + return null; + } + ); + return { account }; + }), + trackAccount: authorizedProcedure + .input(trackAccountInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const resolved = await resolveTwitterAccount(input.username); + if (!resolved) { + throw badRequest(`Could not find the X account @${input.username}`); + } + + const connectedTwitterAccounts = + await db.query.connectedSocialAccounts.findMany({ + columns: { providerAccountId: true, username: true }, + where: and( + eq(connectedSocialAccounts.organizationId, input.organizationId), + eq(connectedSocialAccounts.provider, "twitter") + ), + }); + const existingConnected = connectedTwitterAccounts.some( + (account) => + account.providerAccountId === resolved.providerAccountId || + account.username.toLowerCase() === resolved.username.toLowerCase() + ); + if (existingConnected) { + return { trackedAccountId: null, username: resolved.username }; + } + + const existingTracked = await db.query.trackedSocialAccounts.findFirst({ + columns: { id: true }, + where: and( + eq(trackedSocialAccounts.organizationId, input.organizationId), + eq(trackedSocialAccounts.provider, "twitter"), + eq( + trackedSocialAccounts.providerAccountId, + resolved.providerAccountId + ) + ), + }); + if (existingTracked) { + return { + trackedAccountId: existingTracked.id, + username: resolved.username, + }; + } + + const trackedAccountId = crypto.randomUUID(); + await db.insert(trackedSocialAccounts).values({ + id: trackedAccountId, + organizationId: input.organizationId, + provider: "twitter", + providerAccountId: resolved.providerAccountId, + username: resolved.username, + displayName: resolved.displayName, + profileImageUrl: resolved.profileImageUrl, + verified: resolved.verified, + verifiedType: resolved.verifiedType, + }); + + try { + await syncTrackedAccountNow({ + id: trackedAccountId, + organizationId: input.organizationId, + provider: "twitter", + providerAccountId: resolved.providerAccountId, + username: resolved.username, + displayName: resolved.displayName, + profileImageUrl: resolved.profileImageUrl, + verified: false, + }); + } catch (error) { + console.error("[Analytics] tracked account sync failed:", error); + } + + return { trackedAccountId, username: resolved.username }; + }), + untrackAccount: authorizedProcedure + .input(untrackAccountInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const deleted = await db + .delete(trackedSocialAccounts) + .where( + and( + eq(trackedSocialAccounts.id, input.trackedAccountId), + eq(trackedSocialAccounts.organizationId, input.organizationId) + ) + ) + .returning({ + id: trackedSocialAccounts.id, + provider: trackedSocialAccounts.provider, + providerAccountId: trackedSocialAccounts.providerAccountId, + }); + + const removed = deleted.at(0); + if (!removed) { + throw notFound("Tracked account not found"); + } + + await purgeSocialAccountData({ + organizationId: input.organizationId, + provider: removed.provider, + providerAccountId: removed.providerAccountId, + }).catch((error) => { + console.error("[Analytics] account purge failed:", error); + }); + + return { trackedAccountId: input.trackedAccountId }; + }), followerGrowth: authorizedProcedure .input(analyticsTimeseriesInputSchema) .handler(async ({ context, input }): Promise => { diff --git a/apps/dashboard/src/schemas/analytics.ts b/apps/dashboard/src/schemas/analytics.ts index 92c8bf6cb..9e5b6e815 100644 --- a/apps/dashboard/src/schemas/analytics.ts +++ b/apps/dashboard/src/schemas/analytics.ts @@ -1,4 +1,7 @@ -import { number, object, string } from "zod"; +import { literal, number, object, string, union } from "zod"; + +const LEADERBOARD_USERNAME_MAX_LENGTH = 30; +const LEADING_AT_REGEX = /^@/; export const socialAnalyticsSyncPayloadSchema = object({ organizationId: string().min(1).optional(), @@ -17,3 +20,21 @@ export const analyticsTopPostsInputSchema = object({ organizationId: string().min(1), limit: number().int().min(1).max(50).optional(), }); + +export const leaderboardInputSchema = object({ + organizationId: string().min(1), + days: union([literal(7), literal(30)]).default(7), +}); + +export const trackAccountInputSchema = object({ + organizationId: string().min(1), + username: string() + .min(1) + .max(LEADERBOARD_USERNAME_MAX_LENGTH) + .transform((value) => value.trim().replace(LEADING_AT_REGEX, "")), +}); + +export const untrackAccountInputSchema = object({ + organizationId: string().min(1), + trackedAccountId: string().min(1), +}); diff --git a/apps/dashboard/src/types/analytics.ts b/apps/dashboard/src/types/analytics.ts index 155af9348..0ab4faf58 100644 --- a/apps/dashboard/src/types/analytics.ts +++ b/apps/dashboard/src/types/analytics.ts @@ -1,3 +1,5 @@ +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; + export interface SocialAnalyticsSyncPayload { organizationId?: string; } @@ -49,6 +51,20 @@ export interface TwitterBatchUsersResponse { data?: TwitterBatchUser[]; } +export interface TwitterUserByUsernameResponse { + data?: TwitterBatchUser; +} + +export interface ResolvedTwitterAccount { + providerAccountId: string; + username: string; + displayName: string | null; + profileImageUrl: string | null; + verified: boolean; + verifiedType: string | null; + followersCount: number | null; +} + export interface TwitterTimelineTweet { id: string; text: string; @@ -192,7 +208,97 @@ export interface NotraAdoptionResponse { notraPosts: number; } +export type LeaderboardWindow = 7 | 30; + +export interface LeaderboardAccount { + provider: string; + providerAccountId: string; + username: string; + displayName: string | null; + profileImageUrl: string | null; + verified: boolean; + verifiedType: string | null; + isConnected: boolean; + trackedAccountId: string | null; +} + +export interface LeaderboardWindowTotals { + provider: string; + providerAccountId: string; + posts: number; + interactions: number; + impressions: number; + previousPosts: number; + previousInteractions: number; + previousImpressions: number; +} + +export interface LeaderboardEntry { + key: string; + provider: string; + providerAccountId: string; + username: string; + displayName: string | null; + profileImageUrl: string | null; + verified: boolean; + verifiedType: string | null; + isConnected: boolean; + trackedAccountId: string | null; + rank: number; + previousRank: number | null; + rankChange: number | null; + interactions: number; + impressions: number | null; + posts: number; +} + +export interface LeaderboardResponse { + configured: boolean; + days: LeaderboardWindow; + entries: LeaderboardEntry[]; +} + export interface TimelineMarker { index: number | null; label: string; } + +export interface TrackAccountPreviewResponse { + account: ResolvedTwitterAccount | null; +} + +export interface AnalyticsHeroSummary { + followers: number | null; + impressions: number; + interactions: number; + posts: number; + engagementRate: number | null; +} + +export interface SummaryStatsProps { + accounts: SocialOverviewAccount[]; + points: EngagementTimeseriesPoint[]; +} + +export interface AnalyticsStatTile { + label: string; + value: string; + hint: string; + accent: boolean; +} + +export interface AccountSeriesChartCardProps { + hero?: boolean; + title: string; + readout: string; + kind: "area" | "line" | "bar"; + rows: AccountSeriesRow[]; + config: ChartConfig; + allKeys: string[]; + hiddenKeys: ReadonlySet; + onToggleSeries: (key: string) => void; + hoverIndex: number | null; + onHoverChange: (index: number | null) => void; + markers: TimelineMarker[]; + emptyMessage: string; +} diff --git a/apps/dashboard/src/types/instrument.ts b/apps/dashboard/src/types/instrument.ts new file mode 100644 index 000000000..8c6b79489 --- /dev/null +++ b/apps/dashboard/src/types/instrument.ts @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; + +export interface InstrumentGridProps { + children: ReactNode; + className?: string; +} + +export interface InstrumentModuleProps { + eyebrow: string; + readout?: ReactNode; + action?: ReactNode; + children: ReactNode; + className?: string; + bodyClassName?: string; +} + +export interface InstrumentRevealProps { + active: boolean; + order?: number; + children: ReactNode; + className?: string; +} + +export interface InstrumentEmptyProps { + seed: string; + message: string; + className?: string; +} diff --git a/apps/dashboard/src/utils/analytics-charts.ts b/apps/dashboard/src/utils/analytics-charts.ts index 58d829fa3..023d55b59 100644 --- a/apps/dashboard/src/utils/analytics-charts.ts +++ b/apps/dashboard/src/utils/analytics-charts.ts @@ -1,5 +1,6 @@ import type { AccountSeriesRow, + AnalyticsHeroSummary, EngagementTimeseriesPoint, FollowerGrowthPoint, PostingPerformanceChartRow, @@ -123,3 +124,24 @@ export function buildPostingPerformanceRows( }; }); } + +const PERCENT = 100; + +export function buildAnalyticsHeroSummary( + accounts: SocialOverviewAccount[], + points: EngagementTimeseriesPoint[] +): AnalyticsHeroSummary { + const followers = sumMetric(accounts, (account) => account.followersCount); + let impressions = 0; + let interactions = 0; + let posts = 0; + for (const point of points) { + impressions += point.impressions ?? 0; + interactions += + (point.likes ?? 0) + (point.replies ?? 0) + (point.reposts ?? 0); + posts += point.posts; + } + const engagementRate = + impressions > 0 ? (interactions / impressions) * PERCENT : null; + return { followers, impressions, interactions, posts, engagementRate }; +} diff --git a/apps/dashboard/src/utils/instrument.ts b/apps/dashboard/src/utils/instrument.ts new file mode 100644 index 000000000..7accd5d0a --- /dev/null +++ b/apps/dashboard/src/utils/instrument.ts @@ -0,0 +1,11 @@ +const CLOCK_PAD = 2; + +export function formatSyncClock(timestamp: number | null): string { + if (!timestamp) { + return "--:--"; + } + const date = new Date(timestamp); + return [date.getHours(), date.getMinutes()] + .map((part) => String(part).padStart(CLOCK_PAD, "0")) + .join(":"); +} diff --git a/apps/dashboard/src/workflows/steps/social-analytics-steps.ts b/apps/dashboard/src/workflows/steps/social-analytics-steps.ts index 35a7c6078..854498cec 100644 --- a/apps/dashboard/src/workflows/steps/social-analytics-steps.ts +++ b/apps/dashboard/src/workflows/steps/social-analytics-steps.ts @@ -6,7 +6,10 @@ import { isTinybirdConfigured, } from "@notra/analytics/tinybird/client"; import { db } from "@notra/db/drizzle"; -import { connectedSocialAccounts } from "@notra/db/schema"; +import { + connectedSocialAccounts, + trackedSocialAccounts, +} from "@notra/db/schema"; import { eq } from "drizzle-orm"; import { buildAccountRow } from "@/lib/analytics/rows"; import { collectTwitterRows } from "@/lib/analytics/twitter-sync"; @@ -35,7 +38,22 @@ export async function listSyncableAccounts( : {}), }); - return accounts.map((account) => ({ + const tracked = await db.query.trackedSocialAccounts.findMany({ + columns: { + id: true, + organizationId: true, + provider: true, + providerAccountId: true, + username: true, + displayName: true, + profileImageUrl: true, + }, + ...(organizationId + ? { where: eq(trackedSocialAccounts.organizationId, organizationId) } + : {}), + }); + + const connected: SyncableSocialAccount[] = accounts.map((account) => ({ id: account.id, organizationId: account.organizationId, provider: account.provider, @@ -45,6 +63,38 @@ export async function listSyncableAccounts( profileImageUrl: account.profileImageUrl, verified: account.verified ?? false, })); + + const connectedKeys = new Set( + connected.flatMap((account) => [ + `${account.organizationId}:${account.provider}:${account.providerAccountId}`, + `${account.organizationId}:${account.provider}:@${account.username.toLowerCase()}`, + ]) + ); + + const trackedOnly: SyncableSocialAccount[] = tracked + .filter( + (account) => + !( + connectedKeys.has( + `${account.organizationId}:${account.provider}:${account.providerAccountId}` + ) || + connectedKeys.has( + `${account.organizationId}:${account.provider}:@${account.username.toLowerCase()}` + ) + ) + ) + .map((account) => ({ + id: account.id, + organizationId: account.organizationId, + provider: account.provider, + providerAccountId: account.providerAccountId, + username: account.username, + displayName: account.displayName, + profileImageUrl: account.profileImageUrl, + verified: false, + })); + + return [...connected, ...trackedOnly]; } export async function snapshotAccountDimensions( diff --git a/bun.lock b/bun.lock index 85af5afd8..71741e114 100644 --- a/bun.lock +++ b/bun.lock @@ -387,6 +387,8 @@ "version": "0.0.1", "dependencies": { "@tinybirdco/sdk": "^0.0.82", + "@upstash/redis": "^1.35.8", + "effect": "4.0.0-beta.93", }, "devDependencies": { "@notra/typescript-config": "workspace:*", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 4e8143e95..bac6a1819 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -13,7 +13,9 @@ "./*": "./src/*.ts" }, "dependencies": { - "@tinybirdco/sdk": "^0.0.82" + "@tinybirdco/sdk": "^0.0.82", + "@upstash/redis": "^1.35.8", + "effect": "4.0.0-beta.93" }, "devDependencies": { "@notra/typescript-config": "workspace:*", diff --git a/packages/analytics/src/cache/query-cache.ts b/packages/analytics/src/cache/query-cache.ts new file mode 100644 index 000000000..afc0fd93a --- /dev/null +++ b/packages/analytics/src/cache/query-cache.ts @@ -0,0 +1,96 @@ +import type { Redis } from "@upstash/redis"; +import { Effect } from "effect"; +import { + GLOBAL_SCOPE_ID, + INITIAL_CACHE_VERSION, + QUERY_CACHE_KEY_PREFIX, + QUERY_CACHE_TTL_SECONDS, + VERSION_KEY_PREFIX, +} from "../constants/cache"; +import type { AnalyticsCacheScope, CachedQueryOptions } from "../types/cache"; +import { getAnalyticsRedis } from "./redis"; + +function versionKey( + scope: AnalyticsCacheScope, + organizationId: string | null +): string { + return `${VERSION_KEY_PREFIX}:${scope}:${organizationId ?? GLOBAL_SCOPE_ID}`; +} + +function toJsonSafe(value: unknown): unknown { + return JSON.parse( + JSON.stringify(value, (_key, item) => + typeof item === "bigint" ? Number(item) : item + ) + ); +} + +function stableParams(params: Record): string { + const entries = Object.entries(params) + .filter(([, value]) => value !== undefined) + .sort(([left], [right]) => left.localeCompare(right)); + return JSON.stringify(Object.fromEntries(entries)); +} + +function readVersion( + redis: Redis, + scope: AnalyticsCacheScope, + organizationId: string | null +): Effect.Effect { + return Effect.tryPromise(() => + redis.get(versionKey(scope, organizationId)) + ).pipe( + Effect.orElseSucceed(() => null), + Effect.map((version) => version ?? INITIAL_CACHE_VERSION) + ); +} + +export function cachedQuery( + options: CachedQueryOptions +): Promise { + const redis = getAnalyticsRedis(); + if (!redis) { + return options.fetch(); + } + const program = Effect.gen(function* () { + const version = yield* readVersion( + redis, + options.scope, + options.organizationId + ); + const key = `${QUERY_CACHE_KEY_PREFIX}:${options.scope}:${version}:${options.pipe}:${stableParams(options.params)}`; + const hit = yield* Effect.tryPromise(() => redis.get(key)).pipe( + Effect.orElseSucceed(() => null) + ); + if (hit !== null) { + return hit; + } + const fresh = yield* Effect.tryPromise(() => options.fetch()); + if (fresh !== null) { + yield* Effect.tryPromise(() => + redis.set(key, toJsonSafe(fresh), { ex: QUERY_CACHE_TTL_SECONDS }) + ).pipe(Effect.ignore); + } + return fresh; + }); + return Effect.runPromise(program); +} + +export function bumpAnalyticsVersions( + scope: AnalyticsCacheScope, + organizationIds: ReadonlyArray +): Promise { + const redis = getAnalyticsRedis(); + const keys = [...new Set(organizationIds.map((id) => versionKey(scope, id)))]; + if (!redis || keys.length === 0) { + return Promise.resolve(); + } + const program = Effect.tryPromise(() => { + const pipeline = redis.pipeline(); + for (const key of keys) { + pipeline.incr(key); + } + return pipeline.exec(); + }).pipe(Effect.ignore); + return Effect.runPromise(program); +} diff --git a/packages/analytics/src/cache/redis.ts b/packages/analytics/src/cache/redis.ts new file mode 100644 index 000000000..c2131507d --- /dev/null +++ b/packages/analytics/src/cache/redis.ts @@ -0,0 +1,15 @@ +import { Redis } from "@upstash/redis"; + +let client: Redis | null = null; + +export function getAnalyticsRedis(): Redis | null { + const url = process.env.UPSTASH_REDIS_REST_URL; + const token = process.env.UPSTASH_REDIS_REST_TOKEN; + if (!(url && token)) { + return null; + } + if (!client) { + client = new Redis({ url, token }); + } + return client; +} diff --git a/packages/analytics/src/constants/cache.ts b/packages/analytics/src/constants/cache.ts new file mode 100644 index 000000000..a62b59aed --- /dev/null +++ b/packages/analytics/src/constants/cache.ts @@ -0,0 +1,5 @@ +export const QUERY_CACHE_KEY_PREFIX = "tb:q"; +export const VERSION_KEY_PREFIX = "tb:ver"; +export const QUERY_CACHE_TTL_SECONDS = 21_600; +export const GLOBAL_SCOPE_ID = "global"; +export const INITIAL_CACHE_VERSION = 0; diff --git a/packages/analytics/src/tinybird/client.ts b/packages/analytics/src/tinybird/client.ts index 15d7cc353..b0c66c0ee 100644 --- a/packages/analytics/src/tinybird/client.ts +++ b/packages/analytics/src/tinybird/client.ts @@ -1,4 +1,6 @@ import { type IngestResult, type QueryResult, Tinybird } from "@tinybirdco/sdk"; +import { bumpAnalyticsVersions, cachedQuery } from "../cache/query-cache"; +import type { AnalyticsCacheScope } from "../types/cache"; import { type SocialAccountRow, type SocialAccountStatsRow, @@ -12,6 +14,9 @@ import { socialPosts, } from "./datasources"; import { + type AccountLeaderboardParams, + type AccountLeaderboardRow, + accountLeaderboard, type EngagementTimeseriesParams, type EngagementTimeseriesRow, engagementTimeseries, @@ -54,6 +59,7 @@ function createTinybirdClient() { followerGrowth, postingPerformance, notraAdoption, + accountLeaderboard, }, }); } @@ -72,6 +78,8 @@ function getTinybirdClient() { async function ingestRows( rows: TRow[], + scope: AnalyticsCacheScope, + organizationIds: ReadonlyArray, ingest: ( client: NonNullable>, batch: TRow[] @@ -81,105 +89,168 @@ async function ingestRows( if (!client || rows.length === 0) { return null; } - return await ingest(client, rows); + const result = await ingest(client, rows); + await bumpAnalyticsVersions(scope, organizationIds); + return result; +} + +function cachedPipeQuery, TRow>( + scope: AnalyticsCacheScope, + pipe: string, + params: TParams, + organizationId: string | null, + query: ( + client: NonNullable> + ) => Promise> +): Promise | null> { + const client = getTinybirdClient(); + if (!client) { + return Promise.resolve(null); + } + return cachedQuery({ + scope, + pipe, + organizationId, + params, + fetch: () => query(client), + }); } export function ingestSocialAccounts( rows: SocialAccountRow[] ): Promise { - return ingestRows(rows, (client, batch) => - client.socialAccounts.ingestBatch(batch) + return ingestRows( + rows, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialAccounts.ingestBatch(batch) ); } export function ingestSocialAccountStats( rows: SocialAccountStatsRow[] ): Promise { - return ingestRows(rows, (client, batch) => - client.socialAccountStats.ingestBatch(batch) + return ingestRows( + rows, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialAccountStats.ingestBatch(batch) ); } export function ingestSocialPosts( rows: SocialPostRow[] ): Promise { - return ingestRows(rows, (client, batch) => - client.socialPosts.ingestBatch(batch) + return ingestRows( + rows, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialPosts.ingestBatch(batch) ); } export function ingestSocialPostStats( rows: SocialPostStatsRow[] ): Promise { - return ingestRows(rows, (client, batch) => - client.socialPostStats.ingestBatch(batch) + return ingestRows( + rows, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialPostStats.ingestBatch(batch) ); } export function ingestSocialPostSources( rows: SocialPostSourceRow[] ): Promise { - return ingestRows(rows, (client, batch) => - client.socialPostSources.ingestBatch(batch) + return ingestRows( + rows, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialPostSources.ingestBatch(batch) ); } -export async function querySocialOverview( +export function querySocialOverview( params: SocialOverviewParams ): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.socialOverview.query(params); + return cachedPipeQuery( + "social", + "social_overview", + params, + params.organization_id, + (client) => client.socialOverview.query(params) + ); } -export async function queryEngagementTimeseries( +export function queryEngagementTimeseries( params: EngagementTimeseriesParams ): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.engagementTimeseries.query(params); + return cachedPipeQuery( + "social", + "engagement_timeseries", + params, + params.organization_id, + (client) => client.engagementTimeseries.query(params) + ); } -export async function queryTopPosts( +export function queryTopPosts( params: TopPostsParams ): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.topPosts.query(params); + return cachedPipeQuery( + "social", + "top_posts", + params, + params.organization_id, + (client) => client.topPosts.query(params) + ); } -export async function queryFollowerGrowth( +export function queryFollowerGrowth( params: FollowerGrowthParams ): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.followerGrowth.query(params); + return cachedPipeQuery( + "social", + "follower_growth", + params, + params.organization_id, + (client) => client.followerGrowth.query(params) + ); } -export async function queryPostingPerformance( +export function queryPostingPerformance( params: PostingPerformanceParams ): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.postingPerformance.query(params); + return cachedPipeQuery( + "social", + "posting_performance", + params, + params.organization_id, + (client) => client.postingPerformance.query(params) + ); } -export async function queryNotraAdoption(params: { +export function queryNotraAdoption(params: { organization_id: string; }): Promise | null> { - const client = getTinybirdClient(); - if (!client) { - return null; - } - return await client.notraAdoption.query(params); + return cachedPipeQuery( + "social", + "notra_adoption", + params, + params.organization_id, + (client) => client.notraAdoption.query(params) + ); +} + +export function queryAccountLeaderboard( + params: AccountLeaderboardParams +): Promise | null> { + return cachedPipeQuery( + "social", + "account_leaderboard", + params, + params.organization_id, + (client) => client.accountLeaderboard.query(params) + ); } diff --git a/packages/analytics/src/tinybird/endpoints.ts b/packages/analytics/src/tinybird/endpoints.ts index bd90981b2..bf302626e 100644 --- a/packages/analytics/src/tinybird/endpoints.ts +++ b/packages/analytics/src/tinybird/endpoints.ts @@ -188,6 +188,92 @@ export const engagementTimeseries = defineEndpoint("engagement_timeseries", { }, }); +export const accountLeaderboard = defineEndpoint("account_leaderboard", { + description: + "Per-account interactions for the trailing window and the window before it", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(7).describe("Length of each window in days"), + }, + nodes: [ + node({ + name: "leaderboard_post_metrics", + sql: ` + SELECT + provider, + platform_post_id, + argMax(impressions, captured_at) AS impressions, + argMax(likes, captured_at) AS likes, + argMax(replies, captured_at) AS replies, + argMax(reposts, captured_at) AS reposts + FROM social_post_stats + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "leaderboard_window_posts", + sql: ` + SELECT + provider, + platform_post_id, + argMax(provider_account_id, captured_at) AS provider_account_id, + min(posted_at) AS first_posted_at + FROM social_posts + WHERE organization_id = {{String(organization_id)}} + AND posted_at >= now() - toIntervalDay({{Int32(days, 7)}} * 2) + AND posted_at <= now() + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "leaderboard_totals", + sql: ` + SELECT + window_posts.provider AS provider, + window_posts.provider_account_id AS provider_account_id, + countIf(window_posts.first_posted_at >= now() - toIntervalDay({{Int32(days, 7)}})) AS posts, + sumIf( + coalesce(metrics.likes, 0) + coalesce(metrics.replies, 0) + coalesce(metrics.reposts, 0), + window_posts.first_posted_at >= now() - toIntervalDay({{Int32(days, 7)}}) + ) AS interactions, + sumIf( + coalesce(metrics.impressions, 0), + window_posts.first_posted_at >= now() - toIntervalDay({{Int32(days, 7)}}) + ) AS impressions, + countIf(window_posts.first_posted_at < now() - toIntervalDay({{Int32(days, 7)}})) AS prev_posts, + sumIf( + coalesce(metrics.likes, 0) + coalesce(metrics.replies, 0) + coalesce(metrics.reposts, 0), + window_posts.first_posted_at < now() - toIntervalDay({{Int32(days, 7)}}) + ) AS prev_interactions, + sumIf( + coalesce(metrics.impressions, 0), + window_posts.first_posted_at < now() - toIntervalDay({{Int32(days, 7)}}) + ) AS prev_impressions + FROM leaderboard_window_posts AS window_posts + LEFT JOIN leaderboard_post_metrics AS metrics + ON metrics.provider = window_posts.provider + AND metrics.platform_post_id = window_posts.platform_post_id + GROUP BY window_posts.provider, window_posts.provider_account_id + ORDER BY interactions DESC + `, + }), + ], + output: { + provider: t.string(), + provider_account_id: t.string(), + posts: t.uint64(), + interactions: t.uint64(), + impressions: t.uint64(), + prev_posts: t.uint64(), + prev_interactions: t.uint64(), + prev_impressions: t.uint64(), + }, +}); + +export type AccountLeaderboardParams = InferParams; +export type AccountLeaderboardRow = InferOutputRow; + export const topPosts = defineEndpoint("top_posts", { description: "Best performing posts by latest engagement snapshot", params: { diff --git a/packages/analytics/src/tinybird/purge.ts b/packages/analytics/src/tinybird/purge.ts new file mode 100644 index 000000000..6c082ea51 --- /dev/null +++ b/packages/analytics/src/tinybird/purge.ts @@ -0,0 +1,97 @@ +import { Effect } from "effect"; +import { bumpAnalyticsVersions } from "../cache/query-cache"; +import type { PurgeSocialAccountInput } from "../types/purge"; + +const ACCOUNT_SCOPED_DATASOURCES = [ + "social_accounts", + "social_account_stats", + "social_posts", + "social_post_stats", + "social_post_stats_latest", + "social_account_stats_latest", +]; + +const JOB_POLL_INTERVAL_MS = 1000; +const JOB_POLL_MAX_ATTEMPTS = 60; + +function sanitize(value: string): string { + return value.replace(/['"\\]/g, ""); +} + +function tinybirdBaseUrl(): string { + return process.env.TINYBIRD_BASE_URL ?? "https://api.tinybird.co"; +} + +async function waitForJob(jobId: string): Promise { + for (let attempt = 0; attempt < JOB_POLL_MAX_ATTEMPTS; attempt += 1) { + const response = await fetch(`${tinybirdBaseUrl()}/v0/jobs/${jobId}`, { + headers: { Authorization: `Bearer ${process.env.TINYBIRD_TOKEN}` }, + }); + if (!response.ok) { + throw new Error(`Tinybird job poll failed (${response.status})`); + } + const job: { status?: string; error?: string } = await response.json(); + if (job.status === "done") { + return; + } + if (job.status === "error") { + throw new Error(`Tinybird delete job failed: ${job.error ?? "unknown"}`); + } + await new Promise((resolve) => setTimeout(resolve, JOB_POLL_INTERVAL_MS)); + } + throw new Error("Tinybird delete job timed out"); +} + +async function runDelete(datasource: string, condition: string): Promise { + const response = await fetch( + `${tinybirdBaseUrl()}/v0/datasources/${datasource}/delete`, + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.TINYBIRD_TOKEN}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ delete_condition: condition }), + } + ); + if (!response.ok) { + const detail = await response.text(); + throw new Error( + `Tinybird delete on ${datasource} failed (${response.status}): ${detail}` + ); + } + const payload: { job_id?: string; id?: string } = await response.json(); + const jobId = payload.job_id ?? payload.id; + if (jobId) { + await waitForJob(jobId); + } +} + +function deleteFromDatasource( + datasource: string, + condition: string +): Effect.Effect { + return Effect.tryPromise({ + try: () => runDelete(datasource, condition), + catch: (cause) => + cause instanceof Error ? cause : new Error(String(cause)), + }); +} + +export function purgeSocialAccountData( + input: PurgeSocialAccountInput +): Promise { + if (!process.env.TINYBIRD_TOKEN) { + return Promise.resolve(); + } + const condition = `organization_id = '${sanitize(input.organizationId)}' AND provider = '${sanitize(input.provider)}' AND provider_account_id = '${sanitize(input.providerAccountId)}'`; + const program = Effect.gen(function* () { + for (const datasource of ACCOUNT_SCOPED_DATASOURCES) { + yield* deleteFromDatasource(datasource, condition); + } + yield* Effect.tryPromise(() => + bumpAnalyticsVersions("social", [input.organizationId]) + ); + }); + return Effect.runPromise(program); +} diff --git a/packages/analytics/src/types/cache.ts b/packages/analytics/src/types/cache.ts new file mode 100644 index 000000000..728b2470f --- /dev/null +++ b/packages/analytics/src/types/cache.ts @@ -0,0 +1,9 @@ +export type AnalyticsCacheScope = "social" | "geo" | "model" | "traffic"; + +export interface CachedQueryOptions { + scope: AnalyticsCacheScope; + pipe: string; + organizationId: string | null; + params: Record; + fetch: () => Promise; +} diff --git a/packages/analytics/src/types/purge.ts b/packages/analytics/src/types/purge.ts new file mode 100644 index 000000000..f966d0640 --- /dev/null +++ b/packages/analytics/src/types/purge.ts @@ -0,0 +1,5 @@ +export interface PurgeSocialAccountInput { + organizationId: string; + provider: string; + providerAccountId: string; +} diff --git a/packages/db/migrations/0062_brave_menace.sql b/packages/db/migrations/0062_brave_menace.sql new file mode 100644 index 000000000..617e3fea8 --- /dev/null +++ b/packages/db/migrations/0062_brave_menace.sql @@ -0,0 +1,17 @@ +CREATE TABLE "tracked_social_accounts" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "provider" text NOT NULL, + "provider_account_id" text NOT NULL, + "username" text NOT NULL, + "display_name" text, + "profile_image_url" text, + "verified" boolean DEFAULT false NOT NULL, + "verified_type" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "tracked_social_accounts" ADD CONSTRAINT "tracked_social_accounts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "trackedSocialAccounts_organizationId_idx" ON "tracked_social_accounts" USING btree ("organization_id");--> statement-breakpoint +CREATE UNIQUE INDEX "trackedSocialAccounts_org_provider_account_uidx" ON "tracked_social_accounts" USING btree ("organization_id","provider","provider_account_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0062_snapshot.json b/packages/db/migrations/meta/0062_snapshot.json new file mode 100644 index 000000000..7afb60199 --- /dev/null +++ b/packages/db/migrations/meta/0062_snapshot.json @@ -0,0 +1,8995 @@ +{ + "id": "15722568-6fad-4c25-a22b-220cdc45d546", + "prevId": "2014e8d0-299d-40d4-8e34-286db1c901d8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "accounts_userId_idx": { + "name": "accounts_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_sessions": { + "name": "agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eve_session_id": { + "name": "eve_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "continuation_token": { + "name": "continuation_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_index": { + "name": "stream_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agentSessions_eveSessionId_uidx": { + "name": "agentSessions_eveSessionId_uidx", + "columns": [ + { + "expression": "eve_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agentSessions_organizationId_idx": { + "name": "agentSessions_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agentSessions_chatId_idx": { + "name": "agentSessions_chatId_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_sessions_organization_id_organizations_id_fk": { + "name": "agent_sessions_organization_id_organizations_id_fk", + "tableFrom": "agent_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_sessions_chat_id_chat_sessions_id_fk": { + "name": "agent_sessions_chat_id_chat_sessions_id_fk", + "tableFrom": "agent_sessions", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_actions": { + "name": "autonomy_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_name": { + "name": "capability_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability_version": { + "name": "capability_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "autonomy_action_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "external_ref": { + "name": "external_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyActions_organizationId_idx": { + "name": "autonomyActions_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyActions_runId_idx": { + "name": "autonomyActions_runId_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyActions_org_capability_idempotency_uidx": { + "name": "autonomyActions_org_capability_idempotency_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyActions_organizationId_status_idx": { + "name": "autonomyActions_organizationId_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_actions_organization_id_organizations_id_fk": { + "name": "autonomy_actions_organization_id_organizations_id_fk", + "tableFrom": "autonomy_actions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_actions_run_id_autonomy_runs_id_fk": { + "name": "autonomy_actions_run_id_autonomy_runs_id_fk", + "tableFrom": "autonomy_actions", + "tableTo": "autonomy_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_actions_task_id_autonomy_tasks_id_fk": { + "name": "autonomy_actions_task_id_autonomy_tasks_id_fk", + "tableFrom": "autonomy_actions", + "tableTo": "autonomy_tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_checkpoints": { + "name": "autonomy_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyCheckpoints_organizationId_idx": { + "name": "autonomyCheckpoints_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyCheckpoints_runId_idx": { + "name": "autonomyCheckpoints_runId_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_checkpoints_organization_id_organizations_id_fk": { + "name": "autonomy_checkpoints_organization_id_organizations_id_fk", + "tableFrom": "autonomy_checkpoints", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_checkpoints_run_id_autonomy_runs_id_fk": { + "name": "autonomy_checkpoints_run_id_autonomy_runs_id_fk", + "tableFrom": "autonomy_checkpoints", + "tableTo": "autonomy_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_checkpoints_task_id_autonomy_tasks_id_fk": { + "name": "autonomy_checkpoints_task_id_autonomy_tasks_id_fk", + "tableFrom": "autonomy_checkpoints", + "tableTo": "autonomy_tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_claims": { + "name": "autonomy_claims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claim_key": { + "name": "claim_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_token": { + "name": "owner_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyClaims_scope_claimKey_uidx": { + "name": "autonomyClaims_scope_claimKey_uidx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claim_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyClaims_expiresAt_idx": { + "name": "autonomyClaims_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_claims_organization_id_organizations_id_fk": { + "name": "autonomy_claims_organization_id_organizations_id_fk", + "tableFrom": "autonomy_claims", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_controller_leases": { + "name": "autonomy_controller_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_token": { + "name": "owner_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fencing_token": { + "name": "fencing_token", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyControllerLeases_organizationId_idx": { + "name": "autonomyControllerLeases_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_controller_leases_organization_id_organizations_id_fk": { + "name": "autonomy_controller_leases_organization_id_organizations_id_fk", + "tableFrom": "autonomy_controller_leases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_goals": { + "name": "autonomy_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mandate_id": { + "name": "mandate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "autonomy_goal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "origin_signal_ids": { + "name": "origin_signal_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyGoals_organizationId_idx": { + "name": "autonomyGoals_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyGoals_mandateId_idx": { + "name": "autonomyGoals_mandateId_idx", + "columns": [ + { + "expression": "mandate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyGoals_organizationId_status_idx": { + "name": "autonomyGoals_organizationId_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_goals_organization_id_organizations_id_fk": { + "name": "autonomy_goals_organization_id_organizations_id_fk", + "tableFrom": "autonomy_goals", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_goals_mandate_id_autonomy_mandates_id_fk": { + "name": "autonomy_goals_mandate_id_autonomy_mandates_id_fk", + "tableFrom": "autonomy_goals", + "tableTo": "autonomy_mandates", + "columnsFrom": [ + "mandate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_mandates": { + "name": "autonomy_mandates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "objective": { + "name": "objective", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy": { + "name": "policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "autonomy_mandate_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "qstash_schedule_id": { + "name": "qstash_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyMandates_organizationId_idx": { + "name": "autonomyMandates_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyMandates_organizationId_name_uidx": { + "name": "autonomyMandates_organizationId_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_mandates_organization_id_organizations_id_fk": { + "name": "autonomy_mandates_organization_id_organizations_id_fk", + "tableFrom": "autonomy_mandates", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_mandates_created_by_user_id_users_id_fk": { + "name": "autonomy_mandates_created_by_user_id_users_id_fk", + "tableFrom": "autonomy_mandates", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_outbox": { + "name": "autonomy_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "autonomy_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyOutbox_organizationId_idx": { + "name": "autonomyOutbox_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyOutbox_org_destination_dedupeKey_uidx": { + "name": "autonomyOutbox_org_destination_dedupeKey_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "destination", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyOutbox_status_nextAttemptAt_idx": { + "name": "autonomyOutbox_status_nextAttemptAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_outbox_organization_id_organizations_id_fk": { + "name": "autonomy_outbox_organization_id_organizations_id_fk", + "tableFrom": "autonomy_outbox", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_outbox_run_id_autonomy_runs_id_fk": { + "name": "autonomy_outbox_run_id_autonomy_runs_id_fk", + "tableFrom": "autonomy_outbox", + "tableTo": "autonomy_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_runs": { + "name": "autonomy_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mandate_id": { + "name": "mandate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mandate_version": { + "name": "mandate_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "autonomy_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "planner_input_hash": { + "name": "planner_input_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "planner_output": { + "name": "planner_output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "autonomy_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyRuns_organizationId_idx": { + "name": "autonomyRuns_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyRuns_mandateId_idx": { + "name": "autonomyRuns_mandateId_idx", + "columns": [ + { + "expression": "mandate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyRuns_goalId_idx": { + "name": "autonomyRuns_goalId_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyRuns_organizationId_status_idx": { + "name": "autonomyRuns_organizationId_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_runs_organization_id_organizations_id_fk": { + "name": "autonomy_runs_organization_id_organizations_id_fk", + "tableFrom": "autonomy_runs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_runs_mandate_id_autonomy_mandates_id_fk": { + "name": "autonomy_runs_mandate_id_autonomy_mandates_id_fk", + "tableFrom": "autonomy_runs", + "tableTo": "autonomy_mandates", + "columnsFrom": [ + "mandate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_runs_goal_id_autonomy_goals_id_fk": { + "name": "autonomy_runs_goal_id_autonomy_goals_id_fk", + "tableFrom": "autonomy_runs", + "tableTo": "autonomy_goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_signals": { + "name": "autonomy_signals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "autonomy_signal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "coalesced_into_signal_id": { + "name": "coalesced_into_signal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomySignals_organizationId_idx": { + "name": "autonomySignals_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomySignals_organizationId_dedupeHash_uidx": { + "name": "autonomySignals_organizationId_dedupeHash_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomySignals_organizationId_status_occurredAt_idx": { + "name": "autonomySignals_organizationId_status_occurredAt_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_signals_organization_id_organizations_id_fk": { + "name": "autonomy_signals_organization_id_organizations_id_fk", + "tableFrom": "autonomy_signals", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomySignals_coalescedIntoSignalId_fk": { + "name": "autonomySignals_coalescedIntoSignalId_fk", + "tableFrom": "autonomy_signals", + "tableTo": "autonomy_signals", + "columnsFrom": [ + "coalesced_into_signal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autonomy_tasks": { + "name": "autonomy_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_name": { + "name": "capability_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability_version": { + "name": "capability_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "depends_on_task_ids": { + "name": "depends_on_task_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "autonomy_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "autonomyTasks_organizationId_idx": { + "name": "autonomyTasks_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyTasks_goalId_idx": { + "name": "autonomyTasks_goalId_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyTasks_runId_idx": { + "name": "autonomyTasks_runId_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "autonomyTasks_organizationId_status_waitUntil_idx": { + "name": "autonomyTasks_organizationId_status_waitUntil_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "autonomy_tasks_organization_id_organizations_id_fk": { + "name": "autonomy_tasks_organization_id_organizations_id_fk", + "tableFrom": "autonomy_tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_tasks_goal_id_autonomy_goals_id_fk": { + "name": "autonomy_tasks_goal_id_autonomy_goals_id_fk", + "tableFrom": "autonomy_tasks", + "tableTo": "autonomy_goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autonomy_tasks_run_id_autonomy_runs_id_fk": { + "name": "autonomy_tasks_run_id_autonomy_runs_id_fk", + "tableFrom": "autonomy_tasks", + "tableTo": "autonomy_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_assets": { + "name": "brand_guideline_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "brand_guideline_asset_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aspect_ratio": { + "name": "aspect_ratio", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "brand_guideline_asset_variant", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineAssets_guidelineId_idx": { + "name": "brandGuidelineAssets_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineAssets_guideline_kind_idx": { + "name": "brandGuidelineAssets_guideline_kind_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineAssets_guideline_kind_variant_uidx": { + "name": "brandGuidelineAssets_guideline_kind_variant_uidx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_assets_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_assets_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_assets", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_colors": { + "name": "brand_guideline_colors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "brand_guideline_color_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "light_value": { + "name": "light_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dark_value": { + "name": "dark_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineColors_guidelineId_idx": { + "name": "brandGuidelineColors_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineColors_guideline_role_idx": { + "name": "brandGuidelineColors_guideline_role_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_colors_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_colors_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_colors", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_fonts": { + "name": "brand_guideline_fonts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "brand_guideline_font_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "family": { + "name": "family", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_height": { + "name": "line_height", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineFonts_guidelineId_idx": { + "name": "brandGuidelineFonts_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineFonts_guideline_role_idx": { + "name": "brandGuidelineFonts_guideline_role_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_fonts", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_screenshots": { + "name": "brand_guideline_screenshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "brand_guideline_screenshot_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_page": { + "name": "full_page", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineScreenshots_guidelineId_idx": { + "name": "brandGuidelineScreenshots_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineScreenshots_guideline_kind_uidx": { + "name": "brandGuidelineScreenshots_guideline_kind_uidx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_screenshots", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guideline_tokens": { + "name": "brand_guideline_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guideline_id": { + "name": "guideline_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "brand_guideline_token_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelineTokens_guidelineId_idx": { + "name": "brandGuidelineTokens_guidelineId_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelineTokens_guideline_type_idx": { + "name": "brandGuidelineTokens_guideline_type_idx", + "columns": [ + { + "expression": "guideline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk": { + "name": "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk", + "tableFrom": "brand_guideline_tokens", + "tableTo": "brand_guidelines", + "columnsFrom": [ + "guideline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_guidelines": { + "name": "brand_guidelines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "brand_guideline_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "context_dev_meta": { + "name": "context_dev_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_generation_error": { + "name": "last_generation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandGuidelines_brandSettingsId_uidx": { + "name": "brandGuidelines_brandSettingsId_uidx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandGuidelines_status_idx": { + "name": "brandGuidelines_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_guidelines_brand_settings_id_brand_settings_id_fk": { + "name": "brand_guidelines_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_guidelines", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_references": { + "name": "brand_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reference_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_snapshot_key": { + "name": "source_snapshot_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_hash": { + "name": "source_content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_captured_at": { + "name": "source_captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_document_id": { + "name": "supermemory_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_memory_id": { + "name": "supermemory_memory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supermemory_synced_at": { + "name": "supermemory_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "supermemory_last_sync_error": { + "name": "supermemory_last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicable_to": { + "name": "applicable_to", + "type": "applicable_platform[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['all']::applicable_platform[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandReferences_brandSettingsId_idx": { + "name": "brandReferences_brandSettingsId_idx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandReferences_brandSettingsId_sourceUrl_idx": { + "name": "brandReferences_brandSettingsId_sourceUrl_idx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_references_brand_settings_id_brand_settings_id_fk": { + "name": "brand_references_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_references", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_settings": { + "name": "brand_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Default'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_description": { + "name": "company_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tone_profile": { + "name": "tone_profile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_tone": { + "name": "custom_tone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_instructions": { + "name": "custom_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'English'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSettings_org_name_uidx": { + "name": "brandSettings_org_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSettings_org_default_uidx": { + "name": "brandSettings_org_default_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"brand_settings\".\"is_default\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSettings_organizationId_idx": { + "name": "brandSettings_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_settings_organization_id_organizations_id_fk": { + "name": "brand_settings_organization_id_organizations_id_fk", + "tableFrom": "brand_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "brandSettings_toneProfile_check": { + "name": "brandSettings_toneProfile_check", + "value": "\"brand_settings\".\"tone_profile\" IS NULL OR \"brand_settings\".\"tone_profile\" IN ('Conversational', 'Professional', 'Casual', 'Formal')" + } + }, + "isRLSEnabled": false + }, + "public.brand_sitemap_pages": { + "name": "brand_sitemap_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sitemap_id": { + "name": "sitemap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "brand_sitemap_page_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redirect_target": { + "name": "redirect_target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "text_ratio": { + "name": "text_ratio", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "internal_links": { + "name": "internal_links", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_links": { + "name": "external_links", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "crawled_at": { + "name": "crawled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSitemapPages_sitemapId_idx": { + "name": "brandSitemapPages_sitemapId_idx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemapPages_sitemap_category_idx": { + "name": "brandSitemapPages_sitemap_category_idx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemapPages_sitemap_url_uidx": { + "name": "brandSitemapPages_sitemap_url_uidx", + "columns": [ + { + "expression": "sitemap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk": { + "name": "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk", + "tableFrom": "brand_sitemap_pages", + "tableTo": "brand_sitemaps", + "columnsFrom": [ + "sitemap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand_sitemaps": { + "name": "brand_sitemaps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "brand_settings_id": { + "name": "brand_settings_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "brand_sitemap_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "total_pages": { + "name": "total_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_pages": { + "name": "indexed_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_pages": { + "name": "failed_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "context_dev_meta": { + "name": "context_dev_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_crawl_started_at": { + "name": "last_crawl_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_crawled_at": { + "name": "last_crawled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_crawl_error": { + "name": "last_crawl_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brandSitemaps_brandSettingsId_idx": { + "name": "brandSitemaps_brandSettingsId_idx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "brandSitemaps_brandSettings_url_uidx": { + "name": "brandSitemaps_brandSettings_url_uidx", + "columns": [ + { + "expression": "brand_settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "brand_sitemaps_brand_settings_id_brand_settings_id_fk": { + "name": "brand_sitemaps_brand_settings_id_brand_settings_id_fk", + "tableFrom": "brand_sitemaps", + "tableTo": "brand_settings", + "columnsFrom": [ + "brand_settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_attachments": { + "name": "chat_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatAttachments_organizationId_createdAt_idx": { + "name": "chatAttachments_organizationId_createdAt_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatAttachments_userId_idx": { + "name": "chatAttachments_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_attachments_organization_id_organizations_id_fk": { + "name": "chat_attachments_organization_id_organizations_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_attachments_user_id_users_id_fk": { + "name": "chat_attachments_user_id_users_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_attachments_key_unique": { + "name": "chat_attachments_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "external_channel_source": { + "name": "external_channel_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_channel_id": { + "name": "external_channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatSessions_organizationId_idx": { + "name": "chatSessions_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatSessions_organizationId_deletedAt_idx": { + "name": "chatSessions_organizationId_deletedAt_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatSessions_org_externalChannel_uidx": { + "name": "chatSessions_org_externalChannel_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_channel_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_sessions\".\"external_channel_source\" IN ('discord', 'slack') AND \"chat_sessions\".\"external_channel_id\" IS NOT NULL AND \"chat_sessions\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_sessions_organization_id_organizations_id_fk": { + "name": "chat_sessions_organization_id_organizations_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connected_social_accounts": { + "name": "connected_social_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_type": { + "name": "verified_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connectedSocialAccounts_organizationId_idx": { + "name": "connectedSocialAccounts_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connectedSocialAccounts_org_provider_account_uidx": { + "name": "connectedSocialAccounts_org_provider_account_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connected_social_accounts_organization_id_organizations_id_fk": { + "name": "connected_social_accounts_organization_id_organizations_id_fk", + "tableFrom": "connected_social_accounts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_trigger_lookback_windows": { + "name": "content_trigger_lookback_windows", + "schema": "", + "columns": { + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "window": { + "name": "window", + "type": "lookback_window", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk": { + "name": "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk", + "tableFrom": "content_trigger_lookback_windows", + "tableTo": "content_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_triggers": { + "name": "content_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Untitled Schedule'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_config": { + "name": "source_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "output_type": { + "name": "output_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output_config": { + "name": "output_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qstash_schedule_id": { + "name": "qstash_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_publish": { + "name": "auto_publish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contentTriggers_organizationId_idx": { + "name": "contentTriggers_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contentTriggers_organization_dedupe_uidx": { + "name": "contentTriggers_organization_dedupe_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_triggers_organization_id_organizations_id_fk": { + "name": "content_triggers_organization_id_organizations_id_fk", + "tableFrom": "content_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_app_installations": { + "name": "github_app_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_avatar_url": { + "name": "account_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "githubAppInstallations_organizationId_idx": { + "name": "githubAppInstallations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubAppInstallations_createdByUserId_idx": { + "name": "githubAppInstallations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubAppInstallations_organization_installation_uidx": { + "name": "githubAppInstallations_organization_installation_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_app_installations_organization_id_organizations_id_fk": { + "name": "github_app_installations_organization_id_organizations_id_fk", + "tableFrom": "github_app_installations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_app_installations_created_by_user_id_users_id_fk": { + "name": "github_app_installations_created_by_user_id_users_id_fk", + "tableFrom": "github_app_installations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_integrations": { + "name": "github_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_token": { + "name": "encrypted_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_installation_id": { + "name": "github_app_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_private": { + "name": "github_repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_enabled": { + "name": "repository_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypted_webhook_secret": { + "name": "encrypted_webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "githubIntegrations_organizationId_idx": { + "name": "githubIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubIntegrations_createdByUserId_idx": { + "name": "githubIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "githubIntegrations_organization_owner_repo_uidx": { + "name": "githubIntegrations_organization_owner_repo_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_integrations_organization_id_organizations_id_fk": { + "name": "github_integrations_organization_id_organizations_id_fk", + "tableFrom": "github_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_integrations_created_by_user_id_users_id_fk": { + "name": "github_integrations_created_by_user_id_users_id_fk", + "tableFrom": "github_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_integrations_github_app_installation_id_github_app_installations_id_fk": { + "name": "github_integrations_github_app_installation_id_github_app_installations_id_fk", + "tableFrom": "github_integrations", + "tableTo": "github_app_installations", + "columnsFrom": [ + "github_app_installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.granola_integrations": { + "name": "granola_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "granolaIntegrations_organizationId_idx": { + "name": "granolaIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "granolaIntegrations_createdByUserId_idx": { + "name": "granolaIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "granola_integrations_organization_id_organizations_id_fk": { + "name": "granola_integrations_organization_id_organizations_id_fk", + "tableFrom": "granola_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "granola_integrations_created_by_user_id_users_id_fk": { + "name": "granola_integrations_created_by_user_id_users_id_fk", + "tableFrom": "granola_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitations_organizationId_idx": { + "name": "invitations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_email_idx": { + "name": "invitations_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_inviter_id_users_id_fk": { + "name": "invitations_inviter_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.linear_integrations": { + "name": "linear_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_token": { + "name": "encrypted_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_organization_id": { + "name": "linear_organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linear_organization_name": { + "name": "linear_organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_team_id": { + "name": "linear_team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linear_team_name": { + "name": "linear_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_webhook_secret": { + "name": "encrypted_webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "linearIntegrations_organizationId_idx": { + "name": "linearIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_createdByUserId_idx": { + "name": "linearIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_org_linearOrg_team_uidx": { + "name": "linearIntegrations_org_linearOrg_team_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "linearIntegrations_org_linearOrg_no_team_uidx": { + "name": "linearIntegrations_org_linearOrg_no_team_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "linear_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"linear_integrations\".\"linear_team_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "linear_integrations_organization_id_organizations_id_fk": { + "name": "linear_integrations_organization_id_organizations_id_fk", + "tableFrom": "linear_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "linear_integrations_created_by_user_id_users_id_fk": { + "name": "linear_integrations_created_by_user_id_users_id_fk", + "tableFrom": "linear_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_oauth_credentials": { + "name": "mcp_oauth_credentials", + "schema": "", + "columns": { + "server_integration_id": { + "name": "server_integration_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_tokens": { + "name": "encrypted_tokens", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_client_information": { + "name": "encrypted_client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_authorization_server_information": { + "name": "encrypted_authorization_server_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_refresh_at": { + "name": "access_token_refresh_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connected'" + }, + "token_version": { + "name": "token_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_lease_id": { + "name": "refresh_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_lease_expires_at": { + "name": "refresh_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpOAuthCredentials_organizationId_idx": { + "name": "mcpOAuthCredentials_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpOAuthCredentials_connectedByUserId_idx": { + "name": "mcpOAuthCredentials_connectedByUserId_idx", + "columns": [ + { + "expression": "connected_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_oauth_credentials_server_integration_id_mcp_server_integrations_id_fk": { + "name": "mcp_oauth_credentials_server_integration_id_mcp_server_integrations_id_fk", + "tableFrom": "mcp_oauth_credentials", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_oauth_credentials_organization_id_organizations_id_fk": { + "name": "mcp_oauth_credentials_organization_id_organizations_id_fk", + "tableFrom": "mcp_oauth_credentials", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_oauth_credentials_connected_by_user_id_users_id_fk": { + "name": "mcp_oauth_credentials_connected_by_user_id_users_id_fk", + "tableFrom": "mcp_oauth_credentials", + "tableTo": "users", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpOAuthCredentials_org_server_fk": { + "name": "mcpOAuthCredentials_org_server_fk", + "tableFrom": "mcp_oauth_credentials", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "organization_id", + "server_integration_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcpOAuthCredentials_status_check": { + "name": "mcpOAuthCredentials_status_check", + "value": "\"mcp_oauth_credentials\".\"status\" IN ('connected', 'refreshing', 'reauth_required')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_oauth_pending_authorizations": { + "name": "mcp_oauth_pending_authorizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_integration_id": { + "name": "server_integration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_source_integration_id": { + "name": "store_source_integration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_path": { + "name": "callback_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_code_verifier": { + "name": "encrypted_code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_information": { + "name": "encrypted_client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_authorization_server_information": { + "name": "encrypted_authorization_server_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpOAuthPendingAuthorizations_organizationId_idx": { + "name": "mcpOAuthPendingAuthorizations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpOAuthPendingAuthorizations_userId_idx": { + "name": "mcpOAuthPendingAuthorizations_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpOAuthPendingAuthorizations_serverIntegrationId_idx": { + "name": "mcpOAuthPendingAuthorizations_serverIntegrationId_idx", + "columns": [ + { + "expression": "server_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_idx": { + "name": "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_idx", + "columns": [ + { + "expression": "store_source_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpOAuthPendingAuthorizations_expiresAt_idx": { + "name": "mcpOAuthPendingAuthorizations_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_oauth_pending_authorizations_organization_id_organizations_id_fk": { + "name": "mcp_oauth_pending_authorizations_organization_id_organizations_id_fk", + "tableFrom": "mcp_oauth_pending_authorizations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_oauth_pending_authorizations_user_id_users_id_fk": { + "name": "mcp_oauth_pending_authorizations_user_id_users_id_fk", + "tableFrom": "mcp_oauth_pending_authorizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_oauth_pending_authorizations_server_integration_id_mcp_server_integrations_id_fk": { + "name": "mcp_oauth_pending_authorizations_server_integration_id_mcp_server_integrations_id_fk", + "tableFrom": "mcp_oauth_pending_authorizations", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpOAuthPendingAuthorizations_org_server_fk": { + "name": "mcpOAuthPendingAuthorizations_org_server_fk", + "tableFrom": "mcp_oauth_pending_authorizations", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "organization_id", + "server_integration_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_fk": { + "name": "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_fk", + "tableFrom": "mcp_oauth_pending_authorizations", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "store_source_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mcp_oauth_pending_authorizations_state_hash_unique": { + "name": "mcp_oauth_pending_authorizations_state_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "state_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_integrations": { + "name": "mcp_server_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_light_url": { + "name": "logo_light_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_dark_url": { + "name": "logo_dark_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_featured_at": { + "name": "store_featured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "store_source_integration_id": { + "name": "store_source_integration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_status": { + "name": "store_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "review_note": { + "name": "review_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "encrypted_headers": { + "name": "encrypted_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_tool_sync_at": { + "name": "last_tool_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tool_sync_status": { + "name": "tool_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "tool_sync_error": { + "name": "tool_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "indexed_tool_count": { + "name": "indexed_tool_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpServerIntegrations_resourceType_idx": { + "name": "mcpServerIntegrations_resourceType_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_storeStatus_idx": { + "name": "mcpServerIntegrations_storeStatus_idx", + "columns": [ + { + "expression": "store_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_organizationId_idx": { + "name": "mcpServerIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_createdByUserId_idx": { + "name": "mcpServerIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_storeSourceIntegrationId_idx": { + "name": "mcpServerIntegrations_storeSourceIntegrationId_idx", + "columns": [ + { + "expression": "store_source_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_org_id_uidx": { + "name": "mcpServerIntegrations_org_id_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_org_resourceType_name_uidx": { + "name": "mcpServerIntegrations_org_resourceType_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_org_storeSource_uidx": { + "name": "mcpServerIntegrations_org_storeSource_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "store_source_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_server_integrations\".\"store_source_integration_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpServerIntegrations_storeListing_slug_uidx": { + "name": "mcpServerIntegrations_storeListing_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_server_integrations\".\"resource_type\" = 'store_listing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_integrations_organization_id_organizations_id_fk": { + "name": "mcp_server_integrations_organization_id_organizations_id_fk", + "tableFrom": "mcp_server_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_integrations_created_by_user_id_users_id_fk": { + "name": "mcp_server_integrations_created_by_user_id_users_id_fk", + "tableFrom": "mcp_server_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpServerIntegrations_storeSourceIntegrationId_fk": { + "name": "mcpServerIntegrations_storeSourceIntegrationId_fk", + "tableFrom": "mcp_server_integrations", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "store_source_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcpServerIntegrations_authType_check": { + "name": "mcpServerIntegrations_authType_check", + "value": "\"mcp_server_integrations\".\"auth_type\" IN ('none', 'headers', 'oauth')" + }, + "mcpServerIntegrations_storeStatus_check": { + "name": "mcpServerIntegrations_storeStatus_check", + "value": "\"mcp_server_integrations\".\"store_status\" IN ('draft', 'pending_review', 'live', 'rejected')" + }, + "mcpServerIntegrations_resourceType_check": { + "name": "mcpServerIntegrations_resourceType_check", + "value": "\"mcp_server_integrations\".\"resource_type\" IN ('connection', 'store_listing')" + }, + "mcpServerIntegrations_category_check": { + "name": "mcpServerIntegrations_category_check", + "value": "\"mcp_server_integrations\".\"category\" IS NULL OR \"mcp_server_integrations\".\"category\" IN ('AI', 'Source control', 'Project management', 'Communication', 'Design', 'Notes', 'Deploys', 'Productivity', 'Marketing', 'Publishing')" + }, + "mcpServerIntegrations_resourceState_check": { + "name": "mcpServerIntegrations_resourceState_check", + "value": "(\n (\"mcp_server_integrations\".\"resource_type\" = 'store_listing' AND \"mcp_server_integrations\".\"store_source_integration_id\" IS NULL)\n OR\n (\"mcp_server_integrations\".\"resource_type\" = 'connection' AND \"mcp_server_integrations\".\"store_status\" = 'draft' AND \"mcp_server_integrations\".\"review_note\" IS NULL AND \"mcp_server_integrations\".\"submitted_at\" IS NULL AND \"mcp_server_integrations\".\"reviewed_at\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.mcp_session_tool_activations": { + "name": "mcp_session_tool_activations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_tool_index_id": { + "name": "mcp_tool_index_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_tool_name": { + "name": "runtime_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_query": { + "name": "source_query", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcpSessionToolActivations_session_tool_uidx": { + "name": "mcpSessionToolActivations_session_tool_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "surface", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_tool_index_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpSessionToolActivations_session_idx": { + "name": "mcpSessionToolActivations_session_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "surface", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpSessionToolActivations_expiresAt_idx": { + "name": "mcpSessionToolActivations_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_session_tool_activations_organization_id_organizations_id_fk": { + "name": "mcp_session_tool_activations_organization_id_organizations_id_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk": { + "name": "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "mcp_tool_index", + "columnsFrom": [ + "mcp_tool_index_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpSessionToolActivations_org_tool_fk": { + "name": "mcpSessionToolActivations_org_tool_fk", + "tableFrom": "mcp_session_tool_activations", + "tableTo": "mcp_tool_index", + "columnsFrom": [ + "organization_id", + "mcp_tool_index_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tool_index": { + "name": "mcp_tool_index", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_integration_id": { + "name": "server_integration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_tool_name": { + "name": "server_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_tool_name": { + "name": "runtime_tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_phrase_present": { + "name": "action_phrase_present", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_phrase_past": { + "name": "action_phrase_past", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_indexed_at": { + "name": "last_indexed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcpToolIndex_server_tool_uidx": { + "name": "mcpToolIndex_server_tool_uidx", + "columns": [ + { + "expression": "server_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "server_tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_org_id_uidx": { + "name": "mcpToolIndex_org_id_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_org_runtime_tool_uidx": { + "name": "mcpToolIndex_org_runtime_tool_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "runtime_tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_organizationId_status_idx": { + "name": "mcpToolIndex_organizationId_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_serverIntegrationId_status_idx": { + "name": "mcpToolIndex_serverIntegrationId_status_idx", + "columns": [ + { + "expression": "server_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcpToolIndex_searchText_gin_idx": { + "name": "mcpToolIndex_searchText_gin_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"search_text\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "mcp_tool_index_organization_id_organizations_id_fk": { + "name": "mcp_tool_index_organization_id_organizations_id_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk": { + "name": "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcpToolIndex_org_server_fk": { + "name": "mcpToolIndex_org_server_fk", + "tableFrom": "mcp_tool_index", + "tableTo": "mcp_server_integrations", + "columnsFrom": [ + "organization_id", + "server_integration_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "members_organizationId_idx": { + "name": "members_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_userId_idx": { + "name": "members_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_tokens": { + "name": "oauth_access_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauthAccessTokens_clientId_idx": { + "name": "oauthAccessTokens_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_sessionId_idx": { + "name": "oauthAccessTokens_sessionId_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_userId_idx": { + "name": "oauthAccessTokens_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthAccessTokens_refreshId_idx": { + "name": "oauthAccessTokens_refreshId_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_access_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_session_id_sessions_id_fk": { + "name": "oauth_access_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_tokens_user_id_users_id_fk": { + "name": "oauth_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk": { + "name": "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_refresh_tokens", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_tokens_token_unique": { + "name": "oauth_access_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauthClients_userId_idx": { + "name": "oauthClients_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_clients_user_id_users_id_fk": { + "name": "oauth_clients_user_id_users_id_fk", + "tableFrom": "oauth_clients", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_clients_client_id_unique": { + "name": "oauth_clients_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consents": { + "name": "oauth_consents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauthConsents_clientId_idx": { + "name": "oauthConsents_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthConsents_userId_idx": { + "name": "oauthConsents_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consents_client_id_oauth_clients_client_id_fk": { + "name": "oauth_consents_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consents_user_id_users_id_fk": { + "name": "oauth_consents_user_id_users_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_tokens": { + "name": "oauth_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauthRefreshTokens_clientId_idx": { + "name": "oauthRefreshTokens_clientId_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthRefreshTokens_sessionId_idx": { + "name": "oauthRefreshTokens_sessionId_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauthRefreshTokens_userId_idx": { + "name": "oauthRefreshTokens_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_session_id_sessions_id_fk": { + "name": "oauth_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_tokens_token_unique": { + "name": "oauth_refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.onboarding_suggestions": { + "name": "onboarding_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "onboarding_suggestion_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "onboardingSuggestions_org_type_idx": { + "name": "onboardingSuggestions_org_type_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "onboarding_suggestions_organization_id_organizations_id_fk": { + "name": "onboarding_suggestions_organization_id_organizations_id_fk", + "tableFrom": "onboarding_suggestions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_settings": { + "name": "organization_notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_content_creation": { + "name": "scheduled_content_creation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scheduled_content_failed": { + "name": "scheduled_content_failed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scheduled_content_skipped": { + "name": "scheduled_content_skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_emails": { + "name": "marketing_emails", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "orgNotificationSettings_organizationId_uidx": { + "name": "orgNotificationSettings_organizationId_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_notification_settings_organization_id_organizations_id_fk": { + "name": "organization_notification_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_notification_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heard_about_notra_source": { + "name": "heard_about_notra_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heard_about_notra_other": { + "name": "heard_about_notra_other", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_dismissed": { + "name": "onboarding_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_agent_ran": { + "name": "onboarding_agent_ran", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_agent_started_at": { + "name": "onboarding_agent_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizations_slug_uidx": { + "name": "organizations_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_collections": { + "name": "post_collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "post_collection_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_source": { + "name": "name_source", + "type": "post_collection_name_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'generated'" + }, + "content_types": { + "name": "content_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expected_post_count": { + "name": "expected_post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completed_post_count": { + "name": "completed_post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "post_collections_org_created_at_idx": { + "name": "post_collections_org_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_collections_source_idx": { + "name": "post_collections_source_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_collections_chat_source_uidx": { + "name": "post_collections_chat_source_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"post_collections\".\"source\" = 'chat' AND \"post_collections\".\"source_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_collections_organization_id_organizations_id_fk": { + "name": "post_collections_organization_id_organizations_id_fk", + "tableFrom": "post_collections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recommendations": { + "name": "recommendations", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_org_slug_uidx": { + "name": "posts_org_slug_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"posts\".\"slug\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_org_createdAt_id_idx": { + "name": "posts_org_createdAt_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_collection_id_idx": { + "name": "posts_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_organization_id_organizations_id_fk": { + "name": "posts_organization_id_organizations_id_fk", + "tableFrom": "posts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_collection_id_post_collections_id_fk": { + "name": "posts_collection_id_post_collections_id_fk", + "tableFrom": "posts", + "tableTo": "post_collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repository_outputs": { + "name": "repository_outputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output_type": { + "name": "output_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repositoryOutputs_repositoryId_idx": { + "name": "repositoryOutputs_repositoryId_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositoryOutputs_repository_outputType_uidx": { + "name": "repositoryOutputs_repository_outputType_uidx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "output_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repository_outputs_repository_id_github_integrations_id_fk": { + "name": "repository_outputs_repository_id_github_integrations_id_fk", + "tableFrom": "repository_outputs", + "tableTo": "github_integrations", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_userId_idx": { + "name": "sessions_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_organizationId_idx": { + "name": "skills_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skills_org_name_uidx": { + "name": "skills_org_name_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_organization_id_organizations_id_fk": { + "name": "skills_organization_id_organizations_id_fk", + "tableFrom": "skills", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_integrations": { + "name": "slack_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_bot_token": { + "name": "encrypted_bot_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_bot_user_id": { + "name": "slack_bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_channel_ids": { + "name": "allowed_channel_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slackIntegrations_organizationId_idx": { + "name": "slackIntegrations_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slackIntegrations_createdByUserId_idx": { + "name": "slackIntegrations_createdByUserId_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slackIntegrations_teamId_uidx": { + "name": "slackIntegrations_teamId_uidx", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_integrations_organization_id_organizations_id_fk": { + "name": "slack_integrations_organization_id_organizations_id_fk", + "tableFrom": "slack_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_integrations_created_by_user_id_users_id_fk": { + "name": "slack_integrations_created_by_user_id_users_id_fk", + "tableFrom": "slack_integrations", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracked_social_accounts": { + "name": "tracked_social_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_type": { + "name": "verified_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trackedSocialAccounts_organizationId_idx": { + "name": "trackedSocialAccounts_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trackedSocialAccounts_org_provider_account_uidx": { + "name": "trackedSocialAccounts_org_provider_account_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tracked_social_accounts_organization_id_organizations_id_fk": { + "name": "tracked_social_accounts_organization_id_organizations_id_fk", + "tableFrom": "tracked_social_accounts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hide_personal_data": { + "name": "hide_personal_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_agent_stats": { + "name": "show_agent_stats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verifications_identifier_idx": { + "name": "verifications_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.applicable_platform": { + "name": "applicable_platform", + "schema": "public", + "values": [ + "all", + "twitter", + "linkedin", + "blog" + ] + }, + "public.autonomy_action_status": { + "name": "autonomy_action_status", + "schema": "public", + "values": [ + "pending", + "executing", + "succeeded", + "failed", + "unknown", + "compensated", + "canceled" + ] + }, + "public.autonomy_goal_status": { + "name": "autonomy_goal_status", + "schema": "public", + "values": [ + "open", + "in_progress", + "blocked", + "completed", + "abandoned" + ] + }, + "public.autonomy_mandate_status": { + "name": "autonomy_mandate_status", + "schema": "public", + "values": [ + "active", + "paused", + "revoked" + ] + }, + "public.autonomy_outbox_status": { + "name": "autonomy_outbox_status", + "schema": "public", + "values": [ + "pending", + "attempting", + "delivered", + "failed", + "canceled" + ] + }, + "public.autonomy_run_status": { + "name": "autonomy_run_status", + "schema": "public", + "values": [ + "planning", + "executing", + "completed", + "failed", + "canceled" + ] + }, + "public.autonomy_run_trigger": { + "name": "autonomy_run_trigger", + "schema": "public", + "values": [ + "signal", + "wake", + "manual", + "repair" + ] + }, + "public.autonomy_signal_status": { + "name": "autonomy_signal_status", + "schema": "public", + "values": [ + "pending", + "coalesced", + "processed", + "discarded" + ] + }, + "public.autonomy_task_status": { + "name": "autonomy_task_status", + "schema": "public", + "values": [ + "pending", + "ready", + "running", + "waiting", + "completed", + "failed", + "canceled" + ] + }, + "public.brand_guideline_asset_kind": { + "name": "brand_guideline_asset_kind", + "schema": "public", + "values": [ + "logo", + "wordmark" + ] + }, + "public.brand_guideline_asset_variant": { + "name": "brand_guideline_asset_variant", + "schema": "public", + "values": [ + "light", + "dark" + ] + }, + "public.brand_guideline_color_role": { + "name": "brand_guideline_color_role", + "schema": "public", + "values": [ + "primary", + "secondary", + "accent", + "background", + "foreground", + "neutral", + "custom" + ] + }, + "public.brand_guideline_font_role": { + "name": "brand_guideline_font_role", + "schema": "public", + "values": [ + "heading", + "body", + "button", + "unknown" + ] + }, + "public.brand_guideline_screenshot_kind": { + "name": "brand_guideline_screenshot_kind", + "schema": "public", + "values": [ + "desktop_hero", + "desktop_full_page", + "mobile_hero" + ] + }, + "public.brand_guideline_status": { + "name": "brand_guideline_status", + "schema": "public", + "values": [ + "queued", + "generating", + "ready", + "failed" + ] + }, + "public.brand_guideline_token_type": { + "name": "brand_guideline_token_type", + "schema": "public", + "values": [ + "spacing", + "radius", + "shadow", + "component", + "unknown" + ] + }, + "public.brand_sitemap_page_category": { + "name": "brand_sitemap_page_category", + "schema": "public", + "values": [ + "crawled", + "redirect", + "queued", + "failed" + ] + }, + "public.brand_sitemap_status": { + "name": "brand_sitemap_status", + "schema": "public", + "values": [ + "queued", + "crawling", + "ready", + "failed" + ] + }, + "public.lookback_window": { + "name": "lookback_window", + "schema": "public", + "values": [ + "current_day", + "yesterday", + "last_7_days", + "last_14_days", + "last_30_days" + ] + }, + "public.onboarding_suggestion_type": { + "name": "onboarding_suggestion_type", + "schema": "public", + "values": [ + "schedule_automation", + "event_automation" + ] + }, + "public.post_collection_name_source": { + "name": "post_collection_name_source", + "schema": "public", + "values": [ + "generated", + "user", + "backfill" + ] + }, + "public.post_collection_source": { + "name": "post_collection_source", + "schema": "public", + "values": [ + "manual", + "chat", + "schedule", + "automation", + "api", + "backfill" + ] + }, + "public.post_status": { + "name": "post_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.reference_type": { + "name": "reference_type", + "schema": "public", + "values": [ + "twitter_post", + "linkedin_post", + "blog_post", + "custom" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index a50ae0429..0a225e21b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -435,6 +435,13 @@ "when": 1785580721087, "tag": "0061_lazy_dreadnoughts", "breakpoints": true + }, + { + "idx": 62, + "version": "7", + "when": 1785669058664, + "tag": "0062_brave_menace", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c5fccd2da..6d100f919 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1392,6 +1392,36 @@ export const connectedSocialAccounts = pgTable( ] ); +export const trackedSocialAccounts = pgTable( + "tracked_social_accounts", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + provider: text("provider").notNull(), + providerAccountId: text("provider_account_id").notNull(), + username: text("username").notNull(), + displayName: text("display_name"), + profileImageUrl: text("profile_image_url"), + verified: boolean("verified").notNull().default(false), + verifiedType: text("verified_type"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + index("trackedSocialAccounts_organizationId_idx").on(table.organizationId), + uniqueIndex("trackedSocialAccounts_org_provider_account_uidx").on( + table.organizationId, + table.provider, + table.providerAccountId + ), + ] +); + export const organizationNotificationSettings = pgTable( "organization_notification_settings", { @@ -2416,6 +2446,16 @@ export const connectedSocialAccountsRelations = relations( }) ); +export const trackedSocialAccountsRelations = relations( + trackedSocialAccounts, + ({ one }) => ({ + organization: one(organizations, { + fields: [trackedSocialAccounts.organizationId], + references: [organizations.id], + }), + }) +); + export const organizationNotificationSettingsRelations = relations( organizationNotificationSettings, ({ one }) => ({ diff --git a/packages/ui/dither-kit.json b/packages/ui/dither-kit.json index 4a11f8143..ff7a52320 100644 --- a/packages/ui/dither-kit.json +++ b/packages/ui/dither-kit.json @@ -151,6 +151,46 @@ "hash": "sha256:96d8cad3d7ca87a2a6f092399a60ba2f32612413a190f2885da67854229cdeff" } ] + }, + "pie-chart": { + "version": "0.1.0", + "hash": "sha256:2c08ce4e2ebe3b522541b38f3e37d2b124ab7692c6501225a3558dbf3ea964ca", + "files": [ + { + "path": "components/dither-kit/pie-canvas.tsx", + "hash": "sha256:bcf6f1d8024448b55a3a198afad0ee5db002b2748c5282bb27679422c6a6c2bf" + }, + { + "path": "components/dither-kit/pie-chart.tsx", + "hash": "sha256:2dae2f8e6193e0bca31164ba7d7b829cc5295533cdb207783d6bdb4e346d2c13" + }, + { + "path": "components/dither-kit/pie.tsx", + "hash": "sha256:1e44525a3119913b042b660be50c9c2c0e34a265ee8ed0aa32c65ce4ce9fd655" + } + ] + }, + "radar-chart": { + "version": "0.1.0", + "hash": "sha256:78c9b966e31f80224947f417a7b7f29cc2e367bad865b97c3b5e18fa1f053b0f", + "files": [ + { + "path": "components/dither-kit/radar-canvas.tsx", + "hash": "sha256:be08e9d801c1dc866a3d8eb74fe70208c888a1f6857b584e5f9de631863535ae" + }, + { + "path": "components/dither-kit/radar-chart.tsx", + "hash": "sha256:b62686351465de7d14cf4d919938e005691e8363d45ac8e625cb863a4ad421db" + }, + { + "path": "components/dither-kit/radar-frame.tsx", + "hash": "sha256:a516ed03eb4f5e864532df1f49f265498f1d2c89a676eb93251fad73a6914a67" + }, + { + "path": "components/dither-kit/radar.tsx", + "hash": "sha256:f55dae75073cef3eab55ede93a5fa394711a9c5736046236ebd11623d02849e9" + } + ] } } } diff --git a/packages/ui/src/components/dither-kit/pie-canvas.tsx b/packages/ui/src/components/dither-kit/pie-canvas.tsx new file mode 100644 index 000000000..8b0dfd97c --- /dev/null +++ b/packages/ui/src/components/dither-kit/pie-canvas.tsx @@ -0,0 +1,224 @@ +"use client" + +import { useEffect, useRef } from "react" +import { + BAYER, + backingSize, + bloomLayerStyle, + easeInOutCubic, + OFF_TIER, + prefersReducedMotion, +} from "./dither-paint" +import { rgb } from "./palette" +import { sliceAtAngle } from "./polar" +import { usePolarChart } from "./polar-context" + +const TOP = -Math.PI / 2 +const TAU = Math.PI * 2 +const POP = 6 // px the hovered slice bulges outward + +/** + * Dither canvas for pie / donut charts. Each backing pixel is mapped back to + * plot space, tested for its slice by angle, and filled with the ordered-dither + * scatter — dense at the outer edge, thinning toward the centre — capped by a + * bright arc on the rim. The pie sweeps in clockwise on mount; the hovered slice + * bulges outward with a brighter rim while the rest dim. + */ +export function PieCanvas() { + const ctx = usePolarChart() + const canvasRef = useRef(null) + const bloomRef = useRef(null) + + const { width, height } = ctx.plot + const { cols, rows } = backingSize(width, height) + + // The RAF loop reads the latest ctx through a ref; written in an effect + // (never during render) — mutating a ref mid-render tears under Strict Mode / + // concurrent rendering. + const state = useRef(ctx) + useEffect(() => { + state.current = ctx + }) + + useEffect(() => { + const canvas = canvasRef.current + const c = canvas?.getContext("2d") + if (!(canvas && c) || cols <= 0 || rows <= 0) return + canvas.width = cols + canvas.height = rows + + const bloomCanvas = bloomRef.current + const bloomCtx = bloomCanvas?.getContext("2d") ?? null + if (bloomCanvas) { + bloomCanvas.width = cols + bloomCanvas.height = rows + } + + const reduce = prefersReducedMotion() + const animate = state.current.animate && !reduce + const duration = state.current.animationDuration + let raf = 0 + let animStart = 0 + let lastProg = -1 + let lastRevision = state.current.revision + let intensity = 0 + let popEase = 0 // eases the hovered slice's outward bulge + let needsFill = true + let lastPaintSig = "" + let lastSelected: string | null | undefined = Symbol() as never + let lastHover: number | null | undefined = Symbol() as never + + const paint = (prog: number) => { + const s = state.current + const slices = s.pie + if (!slices) return + c.clearRect(0, 0, cols, rows) + const cx = s.center.x + const cy = s.center.y + const outerR = s.outerRadius + const innerR = s.innerRadius + const revealAngle = TOP + easeInOutCubic(prog) * TAU + + for (let y = 0; y < rows; y++) { + const py = ((y + 0.5) * height) / rows + for (let x = 0; x < cols; x++) { + const px = ((x + 0.5) * width) / cols + const dx = px - cx + const dy = py - cy + const r = Math.hypot(dx, dy) + if (r < innerR) continue + const angle = Math.atan2(dy, dx) + let na = angle + while (na < TOP) na += TAU + while (na >= TOP + TAU) na -= TAU + if (na > revealAngle) continue // clockwise sweep-in + const si = sliceAtAngle(slices, angle) + if (si < 0) continue + const slice = slices[si] + if (!slice) continue + const active = s.hoverIndex === si + const localOuter = active ? outerR + POP * popEase : outerR + if (r > localOuter) continue + + const seed = s.seedOf(slice.name) + const variant = s.variantOf(slice.name) + const emphasis = s.selectedDataKey ?? s.focusDataKey + const selDim = emphasis !== null && emphasis !== slice.name ? 0.3 : 1 + const it = intensity + (active ? 0.4 * popEase : 0) + + // Bright rim on the outer edge — thicker on the hovered slice. + if (localOuter - r < (active ? 1.4 + popEase : 1.4)) { + c.fillStyle = rgb(seed.fill, 1, selDim) + c.fillRect(x, y, 1, 1) + continue + } + const density = (r - innerR) / Math.max(localOuter - innerR, 1) + const bias = variant === "dotted" ? 0.12 : 0 + if (variant === "hatched" && ((x + y) & 3) >= 2) continue + const lit = + variant === "solid" || + density > (BAYER[y & 3]?.[x & 3] ?? 0) - 0.1 * it - bias + if (variant === "dotted" && !lit) continue + // Density → opacity (see the colour-vs-opacity note in dither-paint); + // off cells drop to a faint tier, never a hole to the background. + const k = (0.35 + density * 0.65) * (1 + 0.22 * it) + const alpha = Math.min(1, (lit ? k : k * OFF_TIER) * selDim) + c.fillStyle = rgb(seed.fill, 1, alpha) + c.fillRect(x, y, 1, 1) + } + } + } + + const draw = (now: number) => { + raf = requestAnimationFrame(draw) + const s = state.current + if (!s.ready || !s.pie) return + if (bloomCtx) { + const on = s.bloom !== "off" && (!s.bloomOnHover || s.isMouseInChart) + if (on) { + bloomCtx.clearRect(0, 0, cols, rows) + bloomCtx.drawImage(canvas, 0, 0) + } + } + if (s.revision !== lastRevision) { + lastRevision = s.revision + animStart = 0 + lastProg = -1 + } + if (!animStart) animStart = now + const prog = animate ? Math.min(1, (now - animStart) / duration) : 1 + + const emphasisNow = s.selectedDataKey ?? s.focusDataKey + if (emphasisNow !== lastSelected) { + lastSelected = emphasisNow + needsFill = true + } + if (s.hoverIndex !== lastHover) { + lastHover = s.hoverIndex + popEase = 0 // a freshly-hovered slice bulges out from rest + needsFill = true + } + const itTarget = s.isMouseInChart ? 1 : 0 + if (Math.abs(intensity - itTarget) > 0.001) { + intensity += (itTarget - intensity) * (reduce ? 1 : 0.16) + needsFill = true + } else intensity = itTarget + // Ease the hovered slice's bulge in (and back out when nothing's hovered). + const popTarget = s.hoverIndex != null ? 1 : 0 + if (Math.abs(popEase - popTarget) > 0.001) { + popEase += (popTarget - popEase) * (reduce ? 1 : 0.22) + needsFill = true + } else popEase = popTarget + if (prog !== lastProg) { + lastProg = prog + needsFill = true + } + + // Live tweak repaint (variant, donut inner radius) without re-sweeping. + const paintSig = `${s.innerRadius}|${s.pie + .map((sl) => s.variantOf(sl.name)) + .join(",")}` + if (paintSig !== lastPaintSig) { + lastPaintSig = paintSig + needsFill = true + } + + if (!needsFill) return + paint(prog) + needsFill = false + } + + raf = requestAnimationFrame(draw) + return () => cancelAnimationFrame(raf) + }, [cols, rows, width, height]) + + const bloom = bloomLayerStyle( + ctx.bloom, + ctx.bloomOnHover ? ctx.isMouseInChart : true + ) + const pos = { + left: ctx.margins.left, + top: ctx.margins.top, + width, + height, + } as const + + return ( + <> + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/pie-chart.tsx b/packages/ui/src/components/dither-kit/pie-chart.tsx new file mode 100644 index 000000000..b359586c6 --- /dev/null +++ b/packages/ui/src/components/dither-kit/pie-chart.tsx @@ -0,0 +1,35 @@ +"use client" + +import type { ReactNode } from "react" +import type { ChartConfig, Margins } from "./chart-context" +import type { BloomInput } from "./dither-paint" +import { PieCanvas } from "./pie-canvas" +import { PolarRoot } from "./polar-root" + +// `object` rather than `Record`: interfaces don't get an +// implicit index signature, so interface-typed rows failed to satisfy the +// generic. Internal layers still index rows through their own Row type. +type Row = object + +export type PieChartProps = { + data: TData[] + config: ChartConfig + children: ReactNode + dataKey: string // value field + nameKey: string // slice-name field (looked up in config for colour) + innerRadius?: number // 0–1 ratio for a donut + margins?: Partial + className?: string + animate?: boolean + animationDuration?: number + replayToken?: number + bloom?: BloomInput + bloomOnHover?: boolean + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +} + +/** Composable dither **pie / donut** chart. Compose ``, ``, … inside. */ +export function PieChart(props: PieChartProps) { + return +} diff --git a/packages/ui/src/components/dither-kit/pie.tsx b/packages/ui/src/components/dither-kit/pie.tsx new file mode 100644 index 000000000..0ef9eb6e2 --- /dev/null +++ b/packages/ui/src/components/dither-kit/pie.tsx @@ -0,0 +1,26 @@ +"use client" + +import { useEffect } from "react" +import type { AreaVariant } from "./chart-context" +import { usePolarPart } from "./polar-context" + +export type PieProps = { + /** Fill texture applied to every slice. */ + variant?: AreaVariant +} + +/** + * The pie/donut ring. Slices come from the chart `data` (one per row); this part + * sets the shared fill variant. The dithered wedges are painted on the canvas. + */ +export function Pie({ variant = "gradient" }: PieProps) { + const ctx = usePolarPart("Pie", "pie") + const { registerVariant, unregisterVariant } = ctx + + useEffect(() => { + registerVariant("*", variant) + return () => unregisterVariant("*") + }, [variant, registerVariant, unregisterVariant]) + + return null +} diff --git a/packages/ui/src/components/dither-kit/radar-canvas.tsx b/packages/ui/src/components/dither-kit/radar-canvas.tsx new file mode 100644 index 000000000..364c63731 --- /dev/null +++ b/packages/ui/src/components/dither-kit/radar-canvas.tsx @@ -0,0 +1,241 @@ +"use client" + +import { useEffect, useRef } from "react" +import { + BAYER, + backingSize, + bloomLayerStyle, + easeInOutCubic, + OFF_TIER, + prefersReducedMotion, +} from "./dither-paint" +import { rgb } from "./palette" +import { distToPolygonEdge, pointInPolygon, polarX, polarY } from "./polar" +import { usePolarChart } from "./polar-context" + +/** + * Dither canvas for radar charts. Each series is a closed polygon over the + * spokes (value → radius per axis). Backing pixels inside a polygon are filled + * with the ordered-dither scatter — dense near the polygon edge, thinning toward + * the centre — and each vertex is marked with a bright dot (larger on the hovered + * axis). The polygons scale in from the centre on mount. + */ +export function RadarCanvas() { + const ctx = usePolarChart() + const canvasRef = useRef(null) + const bloomRef = useRef(null) + + const { width, height } = ctx.plot + const { cols, rows } = backingSize(width, height) + + // The RAF loop reads the latest ctx through a ref; written in an effect + // (never during render) — mutating a ref mid-render tears under Strict Mode / + // concurrent rendering. + const state = useRef(ctx) + useEffect(() => { + state.current = ctx + }) + + useEffect(() => { + const canvas = canvasRef.current + const c = canvas?.getContext("2d") + if (!(canvas && c) || cols <= 0 || rows <= 0) return + canvas.width = cols + canvas.height = rows + + const bloomCanvas = bloomRef.current + const bloomCtx = bloomCanvas?.getContext("2d") ?? null + if (bloomCanvas) { + bloomCanvas.width = cols + bloomCanvas.height = rows + } + + const reduce = prefersReducedMotion() + const animate = state.current.animate && !reduce + const duration = state.current.animationDuration + let raf = 0 + let animStart = 0 + let lastProg = -1 + let lastRevision = state.current.revision + let intensity = 0 + let needsFill = true + let lastPaintSig = "" + let lastSelected: string | null | undefined = Symbol() as never + let lastHover: number | null | undefined = Symbol() as never + + const fx = cols / Math.max(width, 1) + const fy = rows / Math.max(height, 1) + + // Build each series polygon in plot coords, scaled by `prog`. + const buildPolys = (prog: number) => { + const s = state.current + const radar = s.radar + if (!radar) return [] + return s.configKeys.map((key) => { + const poly: number[] = [] + const pts: { x: number; y: number }[] = [] + radar.axes.forEach((ax, i) => { + const v = Number(s.data[i]?.[key]) || 0 + const r = (v / radar.max) * s.outerRadius * prog + const x = polarX(s.center.x, r, ax.angle) + const y = polarY(s.center.y, r, ax.angle) + poly.push(x, y) + pts.push({ x, y }) + }) + return { key, poly, pts } + }) + } + + const paint = (prog: number) => { + const s = state.current + if (!s.radar) return + c.clearRect(0, 0, cols, rows) + const polys = buildPolys(easeInOutCubic(prog)) + const band = Math.max(s.outerRadius * 0.45, 1) + + for (let y = 0; y < rows; y++) { + const py = ((y + 0.5) * height) / rows + for (let x = 0; x < cols; x++) { + const px = ((x + 0.5) * width) / cols + // Whether a layer behind already coloured this pixel — front layers + // then keep true gaps in their dither so the back layer shows + // through, instead of tinting the overlap into a muddy blend. + let covered = false + for (let pi = 0; pi < polys.length; pi++) { + const entry = polys[pi] + if (!entry) continue + const { key, poly } = entry + if (!pointInPolygon(px, py, poly)) continue + const seed = s.seedOf(key) + const variant = s.variantOf(key) + const emphasis = s.selectedDataKey ?? s.focusDataKey + const selDim = emphasis !== null && emphasis !== key ? 0.3 : 1 + const dist = distToPolygonEdge(px, py, poly) + if (dist < 1.4) { + c.fillStyle = rgb(seed.fill, 1, selDim) + c.fillRect(x, y, 1, 1) + covered = true + continue + } + const density = 1 - Math.min(1, dist / band) + const bias = variant === "dotted" ? 0.12 : 0 + // Thin each successive (front) layer so overlapping polygons + // read as distinct layers, not a muddy blend. + const sparse = pi * 0.2 + if (variant === "hatched" && ((x + y) & 3) >= 2) continue + const lit = + variant === "solid" || + density > (BAYER[y & 3]?.[x & 3] ?? 0) - 0.1 * intensity - bias + sparse + // Unlit cells: over another layer, stay a real gap (back layer + // shows through); over bare background, paint the faint tier so + // the page never bleeds in. + if (!lit && (variant === "dotted" || covered)) continue + const k = (0.32 + density * 0.68) * (1 + 0.22 * intensity) + const alpha = Math.min(1, (lit ? k : k * OFF_TIER) * selDim) + c.fillStyle = rgb(seed.fill, 1, alpha) + c.fillRect(x, y, 1, 1) + covered = true + } + } + } + + // Vertex markers — larger on the hovered axis. + for (const { key, pts } of polys) { + const seed = s.seedOf(key) + const emphasis = s.selectedDataKey ?? s.focusDataKey + const selDim = emphasis !== null && emphasis !== key ? 0.3 : 1 + pts.forEach((p, i) => { + const bx = Math.round(p.x * fx) + const by = Math.round(p.y * fy) + const big = s.hoverIndex === i + c.fillStyle = rgb(seed.fill, 1, selDim) + const sz = big ? 2 : 1 + c.fillRect(bx - (sz - 1), by - (sz - 1), sz * 2 - 1, sz * 2 - 1) + }) + } + } + + const draw = (now: number) => { + raf = requestAnimationFrame(draw) + const s = state.current + if (!s.ready || !s.radar) return + if (bloomCtx) { + const on = s.bloom !== "off" && (!s.bloomOnHover || s.isMouseInChart) + if (on) { + bloomCtx.clearRect(0, 0, cols, rows) + bloomCtx.drawImage(canvas, 0, 0) + } + } + if (s.revision !== lastRevision) { + lastRevision = s.revision + animStart = 0 + lastProg = -1 + } + if (!animStart) animStart = now + const prog = animate ? Math.min(1, (now - animStart) / duration) : 1 + + const emphasisNow = s.selectedDataKey ?? s.focusDataKey + if (emphasisNow !== lastSelected) { + lastSelected = emphasisNow + needsFill = true + } + if (s.hoverIndex !== lastHover) { + lastHover = s.hoverIndex + needsFill = true + } + const itTarget = s.isMouseInChart ? 1 : 0 + if (Math.abs(intensity - itTarget) > 0.001) { + intensity += (itTarget - intensity) * (reduce ? 1 : 0.16) + needsFill = true + } else intensity = itTarget + if (prog !== lastProg) { + lastProg = prog + needsFill = true + } + + // Live tweak repaint (variant) without replaying the scale-in. + const paintSig = s.configKeys.map((k) => s.variantOf(k)).join(",") + if (paintSig !== lastPaintSig) { + lastPaintSig = paintSig + needsFill = true + } + + if (!needsFill) return + paint(prog) + needsFill = false + } + + raf = requestAnimationFrame(draw) + return () => cancelAnimationFrame(raf) + }, [cols, rows, width, height]) + + const bloom = bloomLayerStyle( + ctx.bloom, + ctx.bloomOnHover ? ctx.isMouseInChart : true + ) + const pos = { + left: ctx.margins.left, + top: ctx.margins.top, + width, + height, + } as const + + return ( + <> + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/radar-chart.tsx b/packages/ui/src/components/dither-kit/radar-chart.tsx new file mode 100644 index 000000000..0561ccf3b --- /dev/null +++ b/packages/ui/src/components/dither-kit/radar-chart.tsx @@ -0,0 +1,42 @@ +"use client" + +import type { ReactNode } from "react" +import type { ChartConfig, Margins } from "./chart-context" +import type { BloomInput } from "./dither-paint" +import { PolarRoot } from "./polar-root" +import { RadarCanvas } from "./radar-canvas" +import { RadarFrame } from "./radar-frame" + +// `object` rather than `Record`: interfaces don't get an +// implicit index signature, so interface-typed rows failed to satisfy the +// generic. Internal layers still index rows through their own Row type. +type Row = object + +export type RadarChartProps = { + data: TData[] + config: ChartConfig + children: ReactNode + nameKey: string // axis-label field + margins?: Partial + className?: string + animate?: boolean + animationDuration?: number + replayToken?: number + bloom?: BloomInput + bloomOnHover?: boolean + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +} + +/** Composable dither **radar** chart. Compose `` series, ``, … inside. */ +export function RadarChart(props: RadarChartProps) { + return ( + } + dataKey="" + {...props} + /> + ) +} diff --git a/packages/ui/src/components/dither-kit/radar-frame.tsx b/packages/ui/src/components/dither-kit/radar-frame.tsx new file mode 100644 index 000000000..d27349354 --- /dev/null +++ b/packages/ui/src/components/dither-kit/radar-frame.tsx @@ -0,0 +1,72 @@ +"use client" + +import { polarX, polarY } from "./polar" +import { usePolarChart } from "./polar-context" + +const LEVELS = 4 + +/** Built-in radar chrome: concentric polygon rings, spokes, and axis labels. + * Rendered behind the dither canvas by the radar root. */ +export function RadarFrame() { + const ctx = usePolarChart() + if (!ctx.ready || !ctx.radar) return null + const { axes } = ctx.radar + const { x: cx, y: cy } = ctx.center + const R = ctx.outerRadius + + const ring = (radius: number) => + `${axes + .map( + (ax, i) => + `${i === 0 ? "M" : "L"}${polarX(cx, radius, ax.angle).toFixed(1)},${polarY(cy, radius, ax.angle).toFixed(1)}` + ) + .join(" ")} Z` + + return ( + + + {Array.from({ length: LEVELS }, (_, l) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: fixed ring levels + + ))} + {axes.map((ax, i) => ( + + ))} + + + {axes.map((ax, i) => { + const lx = polarX(cx, R + 10, ax.angle) + const ly = polarY(cy, R + 10, ax.angle) + const anchor = + Math.abs(Math.cos(ax.angle)) < 0.3 + ? "middle" + : Math.cos(ax.angle) > 0 + ? "start" + : "end" + const hot = ctx.hoverIndex === i + return ( + + {ax.label} + + ) + })} + + + ) +} + +RadarFrame.chartLayer = "back" as const diff --git a/packages/ui/src/components/dither-kit/radar.tsx b/packages/ui/src/components/dither-kit/radar.tsx new file mode 100644 index 000000000..990c6026d --- /dev/null +++ b/packages/ui/src/components/dither-kit/radar.tsx @@ -0,0 +1,32 @@ +"use client" + +import { useEffect } from "react" +import type { AreaVariant } from "./chart-context" +import { usePolarPart } from "./polar-context" + +export type RadarProps = { + dataKey: string + variant?: AreaVariant +} + +/** + * One radar series — a closed polygon over the spokes. Sets this series' fill + * variant; the dithered polygon is painted on the canvas. + */ +export function Radar({ dataKey, variant = "gradient" }: RadarProps) { + const ctx = usePolarPart("Radar", "radar") + const { registerVariant, unregisterVariant } = ctx + + if (process.env.NODE_ENV !== "production" && !ctx.config[dataKey]) { + console.warn( + `: "${dataKey}" is not in the chart \`config\`. Add it so the series has a colour and label.` + ) + } + + useEffect(() => { + registerVariant(dataKey, variant) + return () => unregisterVariant(dataKey) + }, [dataKey, variant, registerVariant, unregisterVariant]) + + return null +} diff --git a/packages/ui/src/components/dither-kit/tooltip.tsx b/packages/ui/src/components/dither-kit/tooltip.tsx index 03736301a..2edf93e46 100644 --- a/packages/ui/src/components/dither-kit/tooltip.tsx +++ b/packages/ui/src/components/dither-kit/tooltip.tsx @@ -23,6 +23,7 @@ export function Tooltip({ valueFormatter, variant = "default", sortItems = "desc", + inlineHeading = false, }: { labelKey?: string valueFormatter?: (value: number, name: string) => string @@ -30,6 +31,9 @@ export function Tooltip({ /** Row ordering: highest value first ("desc"), lowest first ("asc"), or * series registration order ("none"). */ sortItems?: "desc" | "asc" | "none" + /** Single-series categorical charts: drop the separate heading line and use + * the heading value as each row's label. */ + inlineHeading?: boolean }) { const chart = useCommonChart() const show = chart.ready && chart.hoverIndex != null @@ -82,7 +86,7 @@ export function Tooltip({ VARIANT[variant] )} > - {heading && ( + {heading && !inlineHeading && (
{heading}
@@ -98,7 +102,9 @@ export function Tooltip({ className="size-2 rounded-[1px]" style={{ backgroundColor: rgb(item.seed.fill) }} /> - {item.label} + + {inlineHeading && heading ? heading : item.label} + {valueFormatter ? valueFormatter(item.value, item.name)