diff --git a/.env.example b/.env.example index 99ef4f13e..eb2a3cd7f 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,15 @@ UNKEY_API_ID= # Twitter TWITTER_BEARER_TOKEN= +# GEO grounded scanning (optional) +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +PERPLEXITY_API_KEY= + +# Tinybird (social analytics) +TINYBIRD_TOKEN= +TINYBIRD_BASE_URL=https://api.tinybird.co + # Social account linking provider POST_FOR_ME_API_KEY= POST_FOR_ME_API_KEY_TWITTER= @@ -104,3 +113,9 @@ AXIOM_ORG_ID="" # Notra NOTRA_API_KEY="" + +# Beacon (AI traffic detection) +BEACON_INGEST_SECRET= +BEACON_INGEST_URL="http://localhost:3002/api/beacon" +BEACON_ORG_TOKEN= +BEACON_ORG_ID= diff --git a/apps/dashboard/knip.json b/apps/dashboard/knip.json index 6a7ef08ea..e7651f9f9 100644 --- a/apps/dashboard/knip.json +++ b/apps/dashboard/knip.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/knip@6/schema.json", - "entry": ["src/**/*.test.ts"], + "entry": ["src/**/*.test.ts", "scripts/*.ts"], "ignoreIssues": { "src/app/(dashboard)/**/constants/**/*.ts": ["exports"], "src/components/**/*.tsx": ["types"], diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7701b3f3d..34cd91179 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -17,6 +17,9 @@ "smoke:autonomy": "bun run scripts/autonomy-smoke.ts" }, "dependencies": { + "@ai-sdk/anthropic": "3.0.84", + "@ai-sdk/openai": "3.0.71", + "@ai-sdk/perplexity": "3.0.35", "@ai-sdk/react": "3.0.208", "@aws-sdk/client-s3": "^3.990.0", "@aws-sdk/s3-request-presigner": "^3.990.0", @@ -38,6 +41,7 @@ "@lexical/utils": "^0.39.0", "@neoconfetti/react": "^1.0.0", "@notra/ai": "workspace:*", + "@notra/analytics": "workspace:*", "@notra/content-generation": "workspace:*", "@notra/db": "workspace:*", "@notra/email": "workspace:*", diff --git a/apps/dashboard/scripts/register-analytics-schedule.ts b/apps/dashboard/scripts/register-analytics-schedule.ts new file mode 100644 index 000000000..689b0e63f --- /dev/null +++ b/apps/dashboard/scripts/register-analytics-schedule.ts @@ -0,0 +1,26 @@ +import { getAppUrl } from "@notra/ai/qstash/triggers"; +import { Client as QStashClient } from "@upstash/qstash"; + +const HOURLY_CRON = "0 * * * *"; +const SCHEDULE_ID = "social-analytics-sync-hourly"; + +const token = process.env.QSTASH_TOKEN; +if (!token) { + throw new Error("QSTASH_TOKEN is not configured"); +} + +const client = new QStashClient({ token }); +const destination = `${getAppUrl()}/api/workflows/social-analytics-sync`; + +const result = await client.schedules.create({ + scheduleId: SCHEDULE_ID, + destination, + cron: HOURLY_CRON, + body: JSON.stringify({}), + headers: { "Content-Type": "application/json" }, +}); + +console.log( + `Registered schedule ${result.scheduleId ?? SCHEDULE_ID} -> ${destination} (${HOURLY_CRON})` +); +process.exit(0); diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/loading.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/loading.tsx new file mode 100644 index 000000000..8b21d8a63 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/loading.tsx @@ -0,0 +1,5 @@ +import { AnalyticsPageSkeleton } from "./skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx new file mode 100644 index 000000000..66b380f1e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page-client.tsx @@ -0,0 +1,414 @@ +"use client"; + +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { useReducedMotion } from "motion/react"; +import Link from "next/link"; +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 { useOrganizationsContext } from "@/components/providers/organization-provider"; +import { + ACCOUNT_SERIES_COLORS, + ANALYTICS_TIMESERIES_DAYS, +} from "@/constants/analytics"; +import { + useEngagementTimeseries, + useFollowerGrowth, + useNotraAdoption, + usePostingPerformance, + useSocialOverview, + useTopPosts, +} from "@/lib/hooks/use-social-analytics"; +import { cn } from "@/lib/utils"; +import type { TimelineMarker } from "@/types/analytics"; +import { + accountSeriesKey, + buildAccountSeriesRows, + buildPostingPerformanceRows, + 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; +} + +export default function PageClient({ organizationSlug }: PageClientProps) { + const { getOrganization, activeOrganization } = useOrganizationsContext(); + const orgFromList = getOrganization(organizationSlug); + const organization = + activeOrganization?.slug === organizationSlug + ? activeOrganization + : orgFromList; + const organizationId = organization?.id ?? ""; + + const { + data: overview, + isPending: isOverviewPending, + dataUpdatedAt, + } = useSocialOverview(organizationId); + const { data: engagement } = useEngagementTimeseries(organizationId); + const { data: followerGrowth } = useFollowerGrowth(organizationId); + const { data: topPosts } = useTopPosts(organizationId); + const { data: performance } = usePostingPerformance(organizationId); + const { data: adoption } = useNotraAdoption(organizationId); + + 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] + ); + + const accountConfig = useMemo(() => { + const config: ChartConfig = {}; + accounts.forEach((account, index) => { + config[accountSeriesKey(account.provider, account.providerAccountId)] = { + label: `@${account.username}`, + color: + ACCOUNT_SERIES_COLORS[index % ACCOUNT_SERIES_COLORS.length] ?? "blue", + }; + }); + return config; + }, [accounts]); + + const allKeys = useMemo( + () => + accounts.map((account) => + accountSeriesKey(account.provider, account.providerAccountId) + ), + [accounts] + ); + + const visibleKeys = useMemo( + () => allKeys.filter((key) => !hiddenKeys.has(key)), + [allKeys, hiddenKeys] + ); + + const selectedKeys = useMemo(() => new Set(visibleKeys), [visibleKeys]); + + const timelineDays = useMemo( + () => buildTimelineDays(ANALYTICS_TIMESERIES_DAYS), + [] + ); + + const engagementRows = useMemo( + () => + buildAccountSeriesRows( + timelineDays, + visibleKeys, + engagement?.points ?? [], + (point) => + (point.likes ?? 0) + (point.replies ?? 0) + (point.reposts ?? 0) + ), + [timelineDays, visibleKeys, engagement?.points] + ); + + const impressionRows = useMemo( + () => + buildAccountSeriesRows( + timelineDays, + visibleKeys, + engagement?.points ?? [], + (point) => point.impressions ?? 0 + ), + [timelineDays, visibleKeys, engagement?.points] + ); + + const postRows = useMemo( + () => + buildAccountSeriesRows( + timelineDays, + visibleKeys, + engagement?.points ?? [], + (point) => point.posts + ), + [timelineDays, visibleKeys, engagement?.points] + ); + + const markers = useMemo(() => { + const result: TimelineMarker[] = []; + const joined = markerIndexForDate( + timelineDays, + adoption?.organizationCreatedAt ?? null + ); + if (joined !== null) { + result.push({ index: joined, label: "Joined Notra" }); + } + const firstPost = markerIndexForDate( + timelineDays, + adoption?.firstNotraPostAt ?? null + ); + if (firstPost !== null && firstPost !== joined) { + result.push({ index: firstPost, label: "First Notra post" }); + } + return result; + }, [timelineDays, adoption]); + + const performanceRows = useMemo( + () => buildPostingPerformanceRows(performance?.points ?? []), + [performance?.points] + ); + + const toggleAccount = (key: string) => { + setHiddenKeys((previous) => { + const next = new Set(previous); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + if (isOverviewPending) { + return ; + } + + if (accounts.length === 0) { + return ( + +
+
+

+ X + LinkedIn instrument panel +

+

+ Nothing to report yet. +

+
+ + Connect an account + + } + description="Connect an X or LinkedIn account to start tracking followers, impressions, and engagement." + title="No connected accounts" + /> +
+
+ ); + } + + return ( + +
+
+
+
+

+ 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} + > + + + = 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/page.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page.tsx new file mode 100644 index 000000000..9cb7ca3d3 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import PageClient from "./page-client"; +import { AnalyticsPageSkeleton } from "./skeleton"; + +export const metadata: Metadata = { + title: "Analytics", +}; + +async function Page({ + params, +}: { + params: Promise<{ + slug: string; + }>; +}) { + const { slug } = await params; + + return ( + }> + + + ); +} +export default Page; diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx new file mode 100644 index 000000000..9c6972963 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/analytics/skeleton.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Skeleton } from "@notra/ui/components/ui/skeleton"; +import { useId } from "react"; +import { PageContainer } from "@/components/layout/container"; + +const RAIL_TILE_COUNT = 4; +const MODULE_COUNT = 4; + +export function AnalyticsPageSkeleton() { + const id = useId(); + return ( + +
+
+ + +
+
+ {Array.from({ length: RAIL_TILE_COUNT }).map((_, index) => ( + + ))} +
+
+ {Array.from({ length: MODULE_COUNT }).map((_, index) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/experiments/loading.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/loading.tsx new file mode 100644 index 000000000..6900ea4be --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/loading.tsx @@ -0,0 +1,5 @@ +import { ExperimentsPageSkeleton } from "./skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page-client.tsx new file mode 100644 index 000000000..9b4309577 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page-client.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { Add01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Button } from "@notra/ui/components/ui/button"; +import { useState } from "react"; +import { EmptyState } from "@/components/empty-state"; +import { CreateExperimentDialog } from "@/components/experiments/create-experiment-dialog"; +import { ExperimentCard } from "@/components/experiments/experiment-card"; +import { PageContainer } from "@/components/layout/container"; +import { useOrganizationsContext } from "@/components/providers/organization-provider"; +import { useExperiments } from "@/lib/hooks/use-experiments"; +import { ExperimentsPageSkeleton } from "./skeleton"; + +interface PageClientProps { + organizationSlug: string; +} + +export default function PageClient({ organizationSlug }: PageClientProps) { + const { getOrganization, activeOrganization } = useOrganizationsContext(); + const orgFromList = getOrganization(organizationSlug); + const organization = + activeOrganization?.slug === organizationSlug + ? activeOrganization + : orgFromList; + const organizationId = organization?.id ?? ""; + + const [createOpen, setCreateOpen] = useState(false); + const { data, isPending } = useExperiments(organizationId); + + if (isPending) { + return ; + } + + const experiments = data?.experiments ?? []; + const running = experiments.filter( + (experiment) => experiment.status === "running" + ); + const finished = experiments.filter( + (experiment) => experiment.status !== "running" + ); + + return ( + +
+
+
+

A/B Tests

+

+ {running.length} running · compare two posts on one metric +

+
+ +
+ + {experiments.length === 0 ? ( + setCreateOpen(true)}> + Start your first test + + } + description="Pick two published posts and a metric. Notra tracks both until you declare a winner — so every posting decision is backed by data." + title="No experiments yet" + /> + ) : ( + <> + {running.length > 0 && ( +
+ {running.map((experiment) => ( + + ))} +
+ )} + {finished.length > 0 && ( +
+

+ Finished +

+ {finished.map((experiment) => ( + + ))} +
+ )} + + )} + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page.tsx new file mode 100644 index 000000000..1c4141bdd --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import PageClient from "./page-client"; +import { ExperimentsPageSkeleton } from "./skeleton"; + +export const metadata: Metadata = { + title: "A/B Tests", +}; + +async function Page({ + params, +}: { + params: Promise<{ + slug: string; + }>; +}) { + const { slug } = await params; + + return ( + }> + + + ); +} +export default Page; diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/experiments/skeleton.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/skeleton.tsx new file mode 100644 index 000000000..a7161b73f --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/experiments/skeleton.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Skeleton } from "@notra/ui/components/ui/skeleton"; +import { useId } from "react"; +import { PageContainer } from "@/components/layout/container"; + +const CARD_COUNT = 3; + +export function ExperimentsPageSkeleton() { + const id = useId(); + return ( + +
+
+ + +
+ {Array.from({ length: CARD_COUNT }).map((_, index) => ( + + ))} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/loading.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/loading.tsx new file mode 100644 index 000000000..6c552f14e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/loading.tsx @@ -0,0 +1,5 @@ +import { GeoPageSkeleton } from "../skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page-client.tsx new file mode 100644 index 000000000..9d58ce0fe --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page-client.tsx @@ -0,0 +1,81 @@ +"use client"; + +import Link from "next/link"; +import { EmptyState } from "@/components/empty-state"; +import { CompetitorManager } from "@/components/geo/competitor-manager"; +import { CompetitorShareCard } from "@/components/geo/competitor-share-card"; +import { PageContainer } from "@/components/layout/container"; +import { useOrganizationsContext } from "@/components/providers/organization-provider"; +import { useGeoCompetitorShare, useGeoSettings } from "@/lib/hooks/use-geo"; +import { GeoPageSkeleton } from "../skeleton"; + +interface PageClientProps { + organizationSlug: string; +} + +export default function PageClient({ organizationSlug }: PageClientProps) { + const { getOrganization, activeOrganization } = useOrganizationsContext(); + const orgFromList = getOrganization(organizationSlug); + const organization = + activeOrganization?.slug === organizationSlug + ? activeOrganization + : orgFromList; + const organizationId = organization?.id ?? ""; + + const { data: settingsData, isPending } = useGeoSettings(organizationId); + const { data: competitorShare } = useGeoCompetitorShare(organizationId); + + if (isPending) { + return ; + } + + const settings = settingsData?.settings ?? null; + + if (!settings) { + return ( + +
+
+

Competitors

+

+ Who AI engines recommend instead of you +

+
+ + Set up GEO tracking + + } + description="Set up GEO tracking first, then track which competitors AI engines surface." + title="Not set up yet" + /> +
+
+ ); + } + + return ( + +
+
+

Competitors

+

+ Who AI engines recommend instead of you +

+
+ + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page.tsx new file mode 100644 index 000000000..9e2ed824e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/competitors/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { GeoPageSkeleton } from "../skeleton"; +import PageClient from "./page-client"; + +export const metadata: Metadata = { + title: "GEO Competitors", +}; + +async function Page({ + params, +}: { + params: Promise<{ + slug: string; + }>; +}) { + const { slug } = await params; + + return ( + }> + + + ); +} +export default Page; diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/loading.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/loading.tsx new file mode 100644 index 000000000..e7074334b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/loading.tsx @@ -0,0 +1,5 @@ +import { GeoPageSkeleton } from "./skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/page-client.tsx new file mode 100644 index 000000000..b5a42b0fa --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/page-client.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { Settings01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Button } from "@notra/ui/components/ui/button"; +import { Loader2Icon } from "lucide-react"; +import { useReducedMotion } from "motion/react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { EmptyState } from "@/components/empty-state"; +import { AiTrafficCard } from "@/components/geo/ai-traffic-card"; +import { AiTrafficLogCard } from "@/components/geo/ai-traffic-log-card"; +import { EngineRadarCard } from "@/components/geo/engine-radar-card"; +import { GeoSettingsDialog } from "@/components/geo/geo-settings-dialog"; +import { GeoSummaryStats } from "@/components/geo/geo-summary-stats"; +import { LanguagePerformanceCard } from "@/components/geo/language-performance-card"; +import { MentionRateCard } from "@/components/geo/mention-rate-card"; +import { MentionTrendCard } from "@/components/geo/mention-trend-card"; +import { ModelUsageCard } from "@/components/geo/model-usage-card"; +import { PromptResultsPreview } from "@/components/geo/prompt-results-preview"; +import { ShareOfVoiceDonut } from "@/components/geo/share-of-voice-donut"; +import { WebsiteGenerateCard } from "@/components/geo/website-generate-card"; +import { InstrumentGrid } from "@/components/instrument/instrument-grid"; +import { InstrumentReveal } from "@/components/instrument/instrument-reveal"; +import { PageContainer } from "@/components/layout/container"; +import { useOrganizationsContext } from "@/components/providers/organization-provider"; +import { + useAiTraffic, + useBeaconSetup, + useGeoCompetitorShare, + useGeoLanguageShare, + useGeoOverview, + useGeoPromptResults, + useGeoPrompts, + useGeoSettings, + useGeoStartScan, + useGeoTimeseries, + useModelUsage, +} from "@/lib/hooks/use-geo"; +import { cn } from "@/lib/utils"; +import { formatSyncClock } from "@/utils/instrument"; +import { GeoPageSkeleton } from "./skeleton"; + +/* ───────────────────────────────────────────────────────── + * ANIMATION STORYBOARD — GEO instrument panel + * + * Read top-to-bottom. Each `at` value is ms after data mount. + * + * 0ms header + scan controls render statically + * 60ms AI visibility master gauge 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 = { + masterGauge: 60, // AI visibility gauge powers on + modules: 150, // grid modules start staggering in +}; + +const STAGE = { + gauge: 1, // master gauge visible + modules: 2, // grid modules visible +}; + +interface PageClientProps { + organizationSlug: string; +} + +export default function PageClient({ organizationSlug }: PageClientProps) { + const { getOrganization, activeOrganization } = useOrganizationsContext(); + const orgFromList = getOrganization(organizationSlug); + const organization = + activeOrganization?.slug === organizationSlug + ? activeOrganization + : orgFromList; + const organizationId = organization?.id ?? ""; + + const [settingsOpen, setSettingsOpen] = useState(false); + + const { data: settingsData, isPending: isSettingsPending } = + useGeoSettings(organizationId); + const { data: overview, dataUpdatedAt } = useGeoOverview(organizationId); + const { data: timeseries } = useGeoTimeseries(organizationId); + const { data: prompts } = useGeoPrompts(organizationId); + const { data: promptResults } = useGeoPromptResults(organizationId); + const { data: competitorShare } = useGeoCompetitorShare(organizationId); + const { data: languageShare } = useGeoLanguageShare(organizationId); + const { data: modelUsage } = useModelUsage(organizationId); + const { data: aiTraffic } = useAiTraffic(organizationId); + const { data: beaconSetup } = useBeaconSetup(organizationId); + const startScan = useGeoStartScan(organizationId); + + const reduceMotion = useReducedMotion(); + const [stage, setStage] = useState(0); + const ready = !isSettingsPending; + + useEffect(() => { + if (!ready) { + setStage(0); + return; + } + if (reduceMotion) { + setStage(STAGE.modules); + return; + } + const timers: ReturnType[] = []; + timers.push(setTimeout(() => setStage(STAGE.gauge), TIMING.masterGauge)); + timers.push(setTimeout(() => setStage(STAGE.modules), TIMING.modules)); + return () => { + for (const timer of timers) { + clearTimeout(timer); + } + }; + }, [ready, reduceMotion]); + + if (isSettingsPending) { + return ; + } + + const settings = settingsData?.settings ?? null; + + if (!settings) { + return ( + +
+
+

+ AI engine visibility instrument +

+

+ How often do AI engines mention you? +

+
+ + setSettingsOpen(true)} variant="outline"> + Set up manually instead + + } + description="Or tell us your company name, aliases, and competitors yourself. We'll ask the major AI engines the questions your customers ask and track whether you come up." + title="Prefer manual setup?" + /> + +
+
+ ); + } + + return ( + +
+
+
+

GEO

+

+ + How AI engines talk about {settings.companyName} ·{" "} + {startScan.isPending + ? "Scanning" + : `Sync ${formatSyncClock(dataUpdatedAt || null)}`} + +

+
+
+ + +
+
+ + = STAGE.gauge}> + + + + + = STAGE.modules} + className="lg:col-span-8" + order={0} + > + + + = STAGE.modules} + className="lg:col-span-4" + order={1} + > + + + = STAGE.modules} + className="lg:col-span-8" + order={2} + > + + + = STAGE.modules} + className="lg:col-span-4" + order={3} + > + + All competitors + + } + points={competitorShare?.points ?? []} + /> + + = STAGE.modules} + className="lg:col-span-8" + order={4} + > + + All prompts + + } + languages={settings.languages} + results={promptResults?.results ?? []} + /> + + = STAGE.modules} + className="lg:col-span-4" + order={5} + > + + + = STAGE.modules} + className="lg:col-span-4" + order={6} + > + + + = STAGE.modules} + className="lg:col-span-8" + order={7} + > + + + {(aiTraffic?.log.length ?? 0) > 0 && ( + = STAGE.modules} + className="lg:col-span-12" + order={8} + > + + + )} + + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/page.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/page.tsx new file mode 100644 index 000000000..9cb6a12e0 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import PageClient from "./page-client"; +import { GeoPageSkeleton } from "./skeleton"; + +export const metadata: Metadata = { + title: "GEO", +}; + +async function Page({ + params, +}: { + params: Promise<{ + slug: string; + }>; +}) { + const { slug } = await params; + + return ( + }> + + + ); +} +export default Page; diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/loading.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/loading.tsx new file mode 100644 index 000000000..6c552f14e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/loading.tsx @@ -0,0 +1,5 @@ +import { GeoPageSkeleton } from "../skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page-client.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page-client.tsx new file mode 100644 index 000000000..8a81486fe --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page-client.tsx @@ -0,0 +1,75 @@ +"use client"; + +import Link from "next/link"; +import { EmptyState } from "@/components/empty-state"; +import { PromptManager } from "@/components/geo/prompt-manager"; +import { PromptResultsCard } from "@/components/geo/prompt-results-card"; +import { WebsiteGenerateCard } from "@/components/geo/website-generate-card"; +import { PageContainer } from "@/components/layout/container"; +import { useOrganizationsContext } from "@/components/providers/organization-provider"; +import { useGeoPromptResults, useGeoSettings } from "@/lib/hooks/use-geo"; +import { GeoPageSkeleton } from "../skeleton"; + +interface PageClientProps { + organizationSlug: string; +} + +export default function PageClient({ organizationSlug }: PageClientProps) { + const { getOrganization, activeOrganization } = useOrganizationsContext(); + const orgFromList = getOrganization(organizationSlug); + const organization = + activeOrganization?.slug === organizationSlug + ? activeOrganization + : orgFromList; + const organizationId = organization?.id ?? ""; + + const { data: settingsData, isPending } = useGeoSettings(organizationId); + const { data: promptResults } = useGeoPromptResults(organizationId); + + if (isPending) { + return ; + } + + if (!settingsData?.settings) { + return ( + +
+
+

Prompts

+

+ The questions we ask AI engines on your behalf +

+
+ + Set up GEO tracking + + } + description="Set up GEO tracking first, then manage the prompts scanned across AI engines." + title="Not set up yet" + /> +
+
+ ); + } + + return ( + +
+
+

Prompts

+

+ The questions we ask AI engines on your behalf +

+
+ + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page.tsx new file mode 100644 index 000000000..670e4c124 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/prompts/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { GeoPageSkeleton } from "../skeleton"; +import PageClient from "./page-client"; + +export const metadata: Metadata = { + title: "GEO Prompts", +}; + +async function Page({ + params, +}: { + params: Promise<{ + slug: string; + }>; +}) { + const { slug } = await params; + + return ( + }> + + + ); +} +export default Page; diff --git a/apps/dashboard/src/app/(dashboard)/[slug]/geo/skeleton.tsx b/apps/dashboard/src/app/(dashboard)/[slug]/geo/skeleton.tsx new file mode 100644 index 000000000..42ff88491 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/[slug]/geo/skeleton.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Skeleton } from "@notra/ui/components/ui/skeleton"; +import { useId } from "react"; +import { PageContainer } from "@/components/layout/container"; + +const RAIL_TILE_COUNT = 4; +const MODULE_COUNT = 3; + +export function GeoPageSkeleton() { + const id = useId(); + return ( + +
+
+ + +
+
+ {Array.from({ length: RAIL_TILE_COUNT }).map((_, index) => ( + + ))} +
+
+ {Array.from({ length: MODULE_COUNT }).map((_, index) => ( + + ))} +
+ +
+
+ ); +} diff --git a/apps/dashboard/src/app/api/beacon/route.ts b/apps/dashboard/src/app/api/beacon/route.ts new file mode 100644 index 000000000..f3436623d --- /dev/null +++ b/apps/dashboard/src/app/api/beacon/route.ts @@ -0,0 +1,58 @@ +import { ingestAiTrafficEvents } from "@notra/analytics/tinybird/client"; +import { toClickHouseDateTime } from "@notra/analytics/utils/datetime"; +import { type NextRequest, NextResponse } from "next/server"; +import { verifyBeaconToken } from "@/lib/beacon/token"; +import { beaconEventSchema } from "@/schemas/geo"; +import { getClientIp, ratelimit } from "@/utils/ratelimit"; + +const NO_STORE = { "Cache-Control": "no-store" }; + +function toCapturedAt(ts: string): string { + const parsed = new Date(ts); + return toClickHouseDateTime( + Number.isNaN(parsed.getTime()) ? new Date() : parsed + ); +} + +export async function POST(request: NextRequest) { + const { success } = await ratelimit.beaconIngest.limit(getClientIp(request)); + if (!success) { + return NextResponse.json( + { error: "Rate limit exceeded" }, + { status: 429, headers: NO_STORE } + ); + } + + const payload = await request.json().catch(() => null); + const parsed = beaconEventSchema.safeParse(payload); + if (!parsed.success) { + return NextResponse.json( + { error: "Invalid payload" }, + { status: 400, headers: NO_STORE } + ); + } + + const event = parsed.data; + if (!verifyBeaconToken(event.organizationId, event.token)) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: NO_STORE } + ); + } + + await ingestAiTrafficEvents([ + { + organization_id: event.organizationId, + agent: event.agent, + category: event.category, + confidence: event.confidence, + path: event.path, + host: event.host, + method: event.method.toUpperCase(), + referer: event.referer, + captured_at: toCapturedAt(event.ts), + }, + ]); + + return NextResponse.json({ ok: true }, { status: 202, headers: NO_STORE }); +} diff --git a/apps/dashboard/src/app/api/workflows/geo-scan/route.ts b/apps/dashboard/src/app/api/workflows/geo-scan/route.ts new file mode 100644 index 000000000..6a71d7bc3 --- /dev/null +++ b/apps/dashboard/src/app/api/workflows/geo-scan/route.ts @@ -0,0 +1,37 @@ +import { getAppUrl } from "@notra/ai/qstash/triggers"; +import { flattenError } from "zod"; +import { verifyQstashSignature } from "@/lib/workflows/qstash-verify"; +import { startGeoScanRun } from "@/lib/workflows/start"; +import { geoScanPayloadSchema } from "@/schemas/geo"; + +const ROUTE_PATH = "/api/workflows/geo-scan"; + +export async function POST(request: Request) { + const rawBody = await request.text(); + const verified = await verifyQstashSignature({ + request, + rawBody, + url: `${getAppUrl()}${ROUTE_PATH}`, + }); + if (!verified) { + return new Response("Unauthorized", { status: 401 }); + } + + let body: unknown = {}; + if (rawBody) { + try { + body = JSON.parse(rawBody); + } catch { + return new Response("Invalid JSON body", { status: 400 }); + } + } + + const parsed = geoScanPayloadSchema.safeParse(body); + if (!parsed.success) { + console.error("[GEO] Invalid scan payload:", flattenError(parsed.error)); + return new Response("Invalid payload", { status: 400 }); + } + + const { runId } = await startGeoScanRun(parsed.data); + return Response.json({ runId }, { status: 202 }); +} diff --git a/apps/dashboard/src/app/api/workflows/social-analytics-sync/route.ts b/apps/dashboard/src/app/api/workflows/social-analytics-sync/route.ts new file mode 100644 index 000000000..3a59abc95 --- /dev/null +++ b/apps/dashboard/src/app/api/workflows/social-analytics-sync/route.ts @@ -0,0 +1,40 @@ +import { getAppUrl } from "@notra/ai/qstash/triggers"; +import { flattenError } from "zod"; +import { verifyQstashSignature } from "@/lib/workflows/qstash-verify"; +import { startSocialAnalyticsSyncRun } from "@/lib/workflows/start"; +import { socialAnalyticsSyncPayloadSchema } from "@/schemas/analytics"; + +const ROUTE_PATH = "/api/workflows/social-analytics-sync"; + +export async function POST(request: Request) { + const rawBody = await request.text(); + const verified = await verifyQstashSignature({ + request, + rawBody, + url: `${getAppUrl()}${ROUTE_PATH}`, + }); + if (!verified) { + return new Response("Unauthorized", { status: 401 }); + } + + let body: unknown = {}; + if (rawBody) { + try { + body = JSON.parse(rawBody); + } catch { + return new Response("Invalid JSON body", { status: 400 }); + } + } + + const parsed = socialAnalyticsSyncPayloadSchema.safeParse(body); + if (!parsed.success) { + console.error( + "[Social Analytics] Invalid sync payload:", + flattenError(parsed.error) + ); + return new Response("Invalid payload", { status: 400 }); + } + + const { runId } = await startSocialAnalyticsSyncRun(parsed.data); + return Response.json({ runId }, { status: 202 }); +} diff --git a/apps/dashboard/src/components/analytics/account-filter.tsx b/apps/dashboard/src/components/analytics/account-filter.tsx new file mode 100644 index 000000000..e1d9c9ac2 --- /dev/null +++ b/apps/dashboard/src/components/analytics/account-filter.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Linkedin02Icon, NewTwitterIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@notra/ui/components/ui/avatar"; +import { cn } from "@/lib/utils"; +import type { SocialOverviewAccount } from "@/types/analytics"; +import { accountSeriesKey } from "@/utils/analytics-charts"; + +interface AccountFilterProps { + accounts: SocialOverviewAccount[]; + selectedKeys: Set; + onToggle: (key: string) => void; +} + +export function AccountFilter({ + accounts, + selectedKeys, + onToggle, +}: AccountFilterProps) { + return ( +
+ {accounts.map((account) => { + const key = accountSeriesKey( + account.provider, + account.providerAccountId + ); + const selected = selectedKeys.has(key); + return ( + + ); + })} +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/account-series-chart-card.tsx b/apps/dashboard/src/components/analytics/account-series-chart-card.tsx new file mode 100644 index 000000000..d1e1363c4 --- /dev/null +++ b/apps/dashboard/src/components/analytics/account-series-chart-card.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Area, Line } from "@notra/ui/components/dither-kit/area"; +import { + AreaChart, + LineChart, +} from "@notra/ui/components/dither-kit/area-chart"; +import { Bar } from "@notra/ui/components/dither-kit/bar"; +import { BarChart } from "@notra/ui/components/dither-kit/bar-chart"; +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 { ChartSeriesLegend } from "@/components/analytics/chart-legend"; +import { VerticalMarker } from "@/components/analytics/chart-marker"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import type { AccountSeriesChartCardProps } from "@/types/analytics"; + +const CHART_CLASS = "h-56 w-full"; + +function chartFor(kind: "area" | "line" | "bar") { + if (kind === "line") { + return LineChart; + } + if (kind === "bar") { + return BarChart; + } + return AreaChart; +} + +export function AccountSeriesChartCard({ + hero = false, + title, + readout, + kind, + rows, + config, + allKeys, + hiddenKeys, + onToggleSeries, + hoverIndex, + onHoverChange, + markers, + emptyMessage, +}: AccountSeriesChartCardProps) { + const Chart = chartFor(kind); + const seriesKeys = allKeys.filter((key) => !hiddenKeys.has(key)); + const hasData = rows.length > 0 && seriesKeys.length > 0; + + return ( + + {hasData ? ( + + + + + {seriesKeys.map((key) => { + if (kind === "line") { + return ; + } + if (kind === "bar") { + return ; + } + return ; + })} + {markers.map((marker) => ( + + ))} + + + ) : ( + + )} + + + ); +} diff --git a/apps/dashboard/src/components/analytics/chart-legend.tsx b/apps/dashboard/src/components/analytics/chart-legend.tsx new file mode 100644 index 000000000..7bc6ea99a --- /dev/null +++ b/apps/dashboard/src/components/analytics/chart-legend.tsx @@ -0,0 +1,56 @@ +"use client"; + +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { PALETTE, rgb } from "@notra/ui/components/dither-kit/palette"; +import { cn } from "@/lib/utils"; + +interface ChartSeriesLegendProps { + config: ChartConfig; + orderedKeys: string[]; + hiddenKeys: ReadonlySet; + onToggle: (key: string) => void; +} + +export function ChartSeriesLegend({ + config, + orderedKeys, + hiddenKeys, + onToggle, +}: ChartSeriesLegendProps) { + return ( +
+ {orderedKeys.map((key) => { + const entry = config[key]; + if (!entry) { + return null; + } + const hidden = hiddenKeys.has(key); + return ( + + ); + })} +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/chart-marker.tsx b/apps/dashboard/src/components/analytics/chart-marker.tsx new file mode 100644 index 000000000..0eb6efd99 --- /dev/null +++ b/apps/dashboard/src/components/analytics/chart-marker.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useChartPart } from "@notra/ui/components/dither-kit/chart-context"; + +interface VerticalMarkerProps { + index: number | null; + label: string; +} + +export function VerticalMarker({ index, label }: VerticalMarkerProps) { + const ctx = useChartPart("VerticalMarker"); + if (!ctx.ready || index === null || index < 0 || index >= ctx.dataLength) { + return null; + } + + const x = ctx.xCenter(index); + return ( + + + + {label} + + + ); +} diff --git a/apps/dashboard/src/components/analytics/connect-accounts-buttons.tsx b/apps/dashboard/src/components/analytics/connect-accounts-buttons.tsx new file mode 100644 index 000000000..24dd727b8 --- /dev/null +++ b/apps/dashboard/src/components/analytics/connect-accounts-buttons.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { Linkedin02Icon, NewTwitterIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Loader2Icon } from "lucide-react"; +import { useHandleConnectSocialAccount } from "@/lib/hooks/use-connected-accounts"; + +interface ConnectAccountsButtonsProps { + organizationId: string; +} + +export function ConnectAccountsButtons({ + organizationId, +}: ConnectAccountsButtonsProps) { + const twitter = useHandleConnectSocialAccount(organizationId, "twitter"); + const linkedin = useHandleConnectSocialAccount(organizationId, "linkedin"); + + return ( +
+ + Connect + + + +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/followers-card.tsx b/apps/dashboard/src/components/analytics/followers-card.tsx new file mode 100644 index 000000000..2bf0738ac --- /dev/null +++ b/apps/dashboard/src/components/analytics/followers-card.tsx @@ -0,0 +1,147 @@ +"use client"; + +import type { DitherColor } from "@notra/ui/components/dither-kit/palette"; +import { Sparkline } from "@notra/ui/components/dither-kit/sparkline"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@notra/ui/components/ui/avatar"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { cn } from "@/lib/utils"; +import type { + FollowerGrowthPoint, + SocialOverviewAccount, +} from "@/types/analytics"; +import { accountSeriesKey, formatMetric } from "@/utils/analytics-charts"; + +interface FollowersCardProps { + accounts: SocialOverviewAccount[]; + points: FollowerGrowthPoint[]; + hiddenKeys: ReadonlySet; + colorForKey: (key: string) => DitherColor; +} + +function seriesFor( + points: FollowerGrowthPoint[], + provider: string, + providerAccountId: string +): number[] { + return points + .filter( + (point) => + point.provider === provider && + point.providerAccountId === providerAccountId && + point.followersCount !== null + ) + .sort((a, b) => a.day.localeCompare(b.day)) + .map((point) => point.followersCount ?? 0); +} + +function DeltaBadge({ series }: { series: number[] }) { + const first = series.at(0); + const last = series.at(-1); + if (first === undefined || last === undefined || series.length < 2) { + return ( + + tracking started + + ); + } + const delta = last - first; + if (delta === 0) { + return ( + + ±0 + + ); + } + return ( + 0 ? "text-green-500" : "text-red-500" + )} + > + {delta > 0 ? "▲" : "▼"} {Math.abs(delta).toLocaleString()} + + ); +} + +export function FollowersCard({ + accounts, + points, + hiddenKeys, + colorForKey, +}: FollowersCardProps) { + const visible = accounts.filter( + (account) => + !hiddenKeys.has( + accountSeriesKey(account.provider, account.providerAccountId) + ) + ); + + return ( + + {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 new file mode 100644 index 000000000..2b0385039 --- /dev/null +++ b/apps/dashboard/src/components/analytics/posting-performance-card.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { Bar } from "@notra/ui/components/dither-kit/bar"; +import { BarChart } from "@notra/ui/components/dither-kit/bar-chart"; +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +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 { 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 { + rows: PostingPerformanceChartRow[]; +} + +const chartConfig: ChartConfig = { + avgEngagement: { label: "Avg engagement", color: "green" }, + posts: { label: "Posts", color: "grey" }, +}; + +const seriesKeys = Object.keys(chartConfig); + +export function PostingPerformanceCard({ rows }: PostingPerformanceCardProps) { + const [hiddenKeys, setHiddenKeys] = useState>(new Set()); + const visibleKeys = seriesKeys.filter((key) => !hiddenKeys.has(key)); + const hasData = rows.some((row) => row.posts > 0); + + const toggle = (key: string) => { + setHiddenKeys((previous) => { + const next = new Set(previous); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + return ( + + {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 new file mode 100644 index 000000000..7b320e795 --- /dev/null +++ b/apps/dashboard/src/components/analytics/summary-stats.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useMemo } from "react"; +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 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(summary.followers), + hint: "across connected accounts", + accent: false, + }, + { + label: "Impressions", + value: formatMetric(summary.impressions), + hint: "posts from the last 30 days", + accent: false, + }, + { + label: "Interactions", + 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} +

+
+ ))} +
+ ); +} diff --git a/apps/dashboard/src/components/analytics/top-posts-card.tsx b/apps/dashboard/src/components/analytics/top-posts-card.tsx new file mode 100644 index 000000000..2f87c7d7f --- /dev/null +++ b/apps/dashboard/src/components/analytics/top-posts-card.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Linkedin02Icon, NewTwitterIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@notra/ui/components/ui/avatar"; +import { + 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"; + +interface TopPostsCardProps { + posts: TopPostItem[]; +} + +function previewContent(content: string): string { + const singleLine = content.replace(/\s+/g, " ").trim(); + if (singleLine.length <= TOP_POST_CONTENT_PREVIEW_LENGTH) { + return singleLine; + } + return `${singleLine.slice(0, TOP_POST_CONTENT_PREVIEW_LENGTH)}…`; +} + +function PostAvatar({ post }: { post: TopPostItem }) { + const name = post.username ?? post.providerAccountId; + return ( + + {post.profileImageUrl && ( + + )} + + {name.slice(0, 2).toUpperCase()} + + + ); +} + +function PostRow({ post }: { post: TopPostItem }) { + const body = ( +
+ +
+

+ + {post.username ? `@${post.username}` : post.providerAccountId} + · + {formatDayLabel(post.postedAt.slice(0, 10))} +

+

{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)} + +
+ ); + + if (post.url) { + return ( + + {body} + + ); + } + return
{body}
; +} + +export function TopPostsCard({ posts }: TopPostsCardProps) { + return ( + + {posts.length === 0 ? ( + + ) : ( +
+ {posts.map((post) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/command-palette/registry.ts b/apps/dashboard/src/components/command-palette/registry.ts index 8f73df692..d325c5781 100644 --- a/apps/dashboard/src/components/command-palette/registry.ts +++ b/apps/dashboard/src/components/command-palette/registry.ts @@ -1,4 +1,6 @@ import { + AiBrowserIcon, + Analytics01Icon, AnalyticsUpIcon, Calendar03Icon, Comment01Icon, @@ -11,6 +13,7 @@ import { Notification03Icon, PlugIcon, Settings01Icon, + TestTube01Icon, UserCircleIcon, UserGroupIcon, Wallet01Icon, @@ -42,6 +45,38 @@ export const COMMAND_ROUTES: CommandRoute[] = [ section: "Navigation", path: (slug) => `/${slug}/chat`, }, + { + id: "analytics", + label: "Analytics", + keywords: ["stats", "metrics", "followers", "engagement", "social"], + icon: Analytics01Icon, + section: "Navigation", + path: (slug) => `/${slug}/analytics`, + }, + { + id: "geo", + label: "GEO Overview", + keywords: ["ai", "mentions", "visibility", "chatgpt", "share of voice"], + icon: AiBrowserIcon, + section: "Navigation", + path: (slug) => `/${slug}/geo`, + }, + { + id: "geo-prompts", + label: "GEO Prompts", + keywords: ["ai", "prompts", "questions", "tracking"], + icon: AiBrowserIcon, + section: "Navigation", + path: (slug) => `/${slug}/geo/prompts`, + }, + { + id: "geo-competitors", + label: "GEO Competitors", + keywords: ["ai", "competitors", "share of voice", "brands"], + icon: AiBrowserIcon, + section: "Navigation", + path: (slug) => `/${slug}/geo/competitors`, + }, { id: "content", label: "Content", @@ -74,6 +109,14 @@ export const COMMAND_ROUTES: CommandRoute[] = [ section: "Automation", path: (slug) => `/${slug}/automation/schedules`, }, + { + id: "experiments", + label: "A/B Tests", + keywords: ["experiments", "ab test", "variants", "testing"], + icon: TestTube01Icon, + section: "Automation", + path: (slug) => `/${slug}/experiments`, + }, { id: "automation-events", label: "Events", diff --git a/apps/dashboard/src/components/dashboard/nav-main.tsx b/apps/dashboard/src/components/dashboard/nav-main.tsx index 92a51e0d9..3cc2376b3 100644 --- a/apps/dashboard/src/components/dashboard/nav-main.tsx +++ b/apps/dashboard/src/components/dashboard/nav-main.tsx @@ -1,8 +1,12 @@ "use client"; import { useFlag } from "@databuddy/sdk/react"; import { + AiBrowserIcon, + AiChat01Icon, + Analytics01Icon, AnalyticsUpIcon, Calendar03Icon, + ChartAnalysisIcon, Home01Icon, Key01Icon, MagicWand01Icon, @@ -12,6 +16,7 @@ import { PlugIcon, RainbowIcon, SearchIcon, + TestTube01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Badge } from "@notra/ui/components/ui/badge"; @@ -38,6 +43,7 @@ import { NavBrandIdentity } from "./nav-brand-identity"; const categoryLabels: Record, string> = { workspace: "Workspace", automation: "Automation", + geo: "GEO", manage: "Manage", }; @@ -61,6 +67,30 @@ const navMainItems: NavMainItem[] = [ label: "Content", category: "workspace", }, + { + link: "/analytics", + icon: Analytics01Icon, + label: "Analytics", + category: "workspace", + }, + { + link: "/geo", + icon: AiBrowserIcon, + label: "Overview", + category: "geo", + }, + { + link: "/geo/prompts", + icon: AiChat01Icon, + label: "Prompts", + category: "geo", + }, + { + link: "/geo/competitors", + icon: ChartAnalysisIcon, + label: "Competitors", + category: "geo", + }, { link: "/skills", icon: MagicWand01Icon, @@ -85,6 +115,12 @@ const navMainItems: NavMainItem[] = [ label: "Events", category: "automation", }, + { + link: "/experiments", + icon: TestTube01Icon, + label: "A/B Tests", + category: "automation", + }, { link: "/api-keys", icon: Key01Icon, @@ -109,6 +145,7 @@ const itemsByCategory: Record = { none: [], workspace: [], automation: [], + geo: [], manage: [], }; for (const item of navMainItems) { diff --git a/apps/dashboard/src/components/experiments/create-experiment-dialog.tsx b/apps/dashboard/src/components/experiments/create-experiment-dialog.tsx new file mode 100644 index 000000000..fb41d95a8 --- /dev/null +++ b/apps/dashboard/src/components/experiments/create-experiment-dialog.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { + ResponsiveDialog, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, +} from "@notra/ui/components/shared/responsive-dialog"; +import { Button } from "@notra/ui/components/ui/button"; +import { Input } from "@notra/ui/components/ui/input"; +import { Label } from "@notra/ui/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@notra/ui/components/ui/select"; +import { Loader2Icon } from "lucide-react"; +import { useId, useState } from "react"; +import { + EXPERIMENT_METRIC_LABELS, + EXPERIMENT_METRICS, + EXPERIMENT_POST_PICKER_LIMIT, +} from "@/constants/experiments"; +import { useExperimentCreate } from "@/lib/hooks/use-experiments"; +import { useTopPosts } from "@/lib/hooks/use-social-analytics"; +import type { ExperimentMetric } from "@/types/experiments"; + +interface CreateExperimentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + organizationId: string; +} + +const POST_LABEL_LENGTH = 60; + +function postLabel(content: string): string { + const singleLine = content.replace(/\s+/g, " ").trim(); + if (singleLine.length <= POST_LABEL_LENGTH) { + return singleLine; + } + return `${singleLine.slice(0, POST_LABEL_LENGTH)}…`; +} + +export function CreateExperimentDialog({ + open, + onOpenChange, + organizationId, +}: CreateExperimentDialogProps) { + const id = useId(); + const [name, setName] = useState(""); + const [metric, setMetric] = useState("engagement"); + const [variantAPostId, setVariantAPostId] = useState(""); + const [variantBPostId, setVariantBPostId] = useState(""); + const create = useExperimentCreate(organizationId); + const { data: topPosts } = useTopPosts( + organizationId, + EXPERIMENT_POST_PICKER_LIMIT + ); + + const posts = topPosts?.posts ?? []; + const variantAPost = posts.find( + (post) => post.platformPostId === variantAPostId + ); + + const canCreate = + name.trim().length > 0 && + variantAPostId.length > 0 && + variantBPostId.length > 0 && + variantAPostId !== variantBPostId; + + const handleCreate = () => { + if (!canCreate) { + return; + } + create.mutate( + { + name: name.trim(), + provider: variantAPost?.provider ?? "twitter", + variantAPostId, + variantBPostId, + metric, + }, + { + onSuccess: () => { + setName(""); + setVariantAPostId(""); + setVariantBPostId(""); + onOpenChange(false); + }, + } + ); + }; + + return ( + + + + New A/B test + + Compare two published posts on one metric. Metrics update on every + sync until you declare a winner. + + +
+
+ + setName(event.target.value)} + placeholder="Hook style: question vs statement" + value={name} + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +} diff --git a/apps/dashboard/src/components/experiments/experiment-card.tsx b/apps/dashboard/src/components/experiments/experiment-card.tsx new file mode 100644 index 000000000..22add9dcb --- /dev/null +++ b/apps/dashboard/src/components/experiments/experiment-card.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { Badge } from "@notra/ui/components/ui/badge"; +import { Button } from "@notra/ui/components/ui/button"; +import { Card, CardContent, CardHeader } from "@notra/ui/components/ui/card"; +import { EXPERIMENT_METRIC_LABELS } from "@/constants/experiments"; +import { + useExperimentCancel, + useExperimentComplete, + useExperimentRemove, +} from "@/lib/hooks/use-experiments"; +import { cn } from "@/lib/utils"; +import type { + ExperimentItem, + ExperimentVariantStats, +} from "@/types/experiments"; +import { formatMetric } from "@/utils/analytics-charts"; + +interface ExperimentCardProps { + experiment: ExperimentItem; + organizationId: string; +} + +const CONTENT_PREVIEW_LENGTH = 80; + +function previewContent(content: string | null): string { + if (!content) { + return "Post not tracked yet"; + } + const singleLine = content.replace(/\s+/g, " ").trim(); + if (singleLine.length <= CONTENT_PREVIEW_LENGTH) { + return singleLine; + } + return `${singleLine.slice(0, CONTENT_PREVIEW_LENGTH)}…`; +} + +function statusBadge(experiment: ExperimentItem) { + if (experiment.status === "running") { + return Running; + } + if (experiment.status === "cancelled") { + return Cancelled; + } + if (experiment.winner === "tie") { + return Tie; + } + return Winner: {experiment.winner?.toUpperCase() ?? "?"}; +} + +function VariantRow({ + label, + variant, + otherValue, + highlighted, +}: { + label: string; + variant: ExperimentVariantStats; + otherValue: number | null; + highlighted: boolean; +}) { + const value = variant.value ?? 0; + const max = Math.max(value, otherValue ?? 0, 1); + const widthPercent = Math.max(Math.round((value / max) * 100), 4); + + const body = ( +
+
+ + Variant {label} + + + {variant.value === null ? "N/A" : formatMetric(variant.value)} + +
+

{previewContent(variant.content)}

+
+
+
+
+ ); + + if (variant.url) { + return ( + + {body} + + ); + } + return body; +} + +export function ExperimentCard({ + experiment, + organizationId, +}: ExperimentCardProps) { + const complete = useExperimentComplete(organizationId); + const cancel = useExperimentCancel(organizationId); + const remove = useExperimentRemove(organizationId); + + const leader = + experiment.status === "completed" ? experiment.winner : experiment.leader; + + return ( + + +
+

{experiment.name}

+

+ {EXPERIMENT_METRIC_LABELS[experiment.metric] ?? experiment.metric} + {experiment.liftPercent !== null && + experiment.liftPercent > 0 && + ` · ${experiment.liftPercent}% lift`} + {experiment.hypothesis && ` · ${experiment.hypothesis}`} +

+
+ {statusBadge(experiment)} +
+ +
+ + +
+
+ {experiment.status === "running" ? ( + <> + + + + ) : ( + + )} +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/ai-traffic-card.tsx b/apps/dashboard/src/components/geo/ai-traffic-card.tsx new file mode 100644 index 000000000..461b1ed4d --- /dev/null +++ b/apps/dashboard/src/components/geo/ai-traffic-card.tsx @@ -0,0 +1,158 @@ +"use client"; + +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { Pie } from "@notra/ui/components/dither-kit/pie"; +import { PieChart } from "@notra/ui/components/dither-kit/pie-chart"; +import { Tooltip as DitherTooltip } from "@notra/ui/components/dither-kit/tooltip"; +import { useMemo } from "react"; +import { PurposeBadge } from "@/components/geo/purpose-badge"; +import { InstrumentModule } from "@/components/instrument/instrument-module"; +import { AI_TRAFFIC_PURPOSE_LABELS } from "@/constants/geo"; +import type { + AiTrafficAgent, + AiTrafficResponse, + BeaconSetupResponse, +} from "@/types/geo"; +import { formatAiTrafficTimestamp, hitBarWidth } from "@/utils/ai-traffic"; + +interface AiTrafficCardProps { + traffic: AiTrafficResponse | undefined; + setup: BeaconSetupResponse | undefined; +} + +function AgentRow({ + agent, + maxHits, +}: { + agent: AiTrafficAgent; + maxHits: number; +}) { + return ( +
+
+ + + {agent.agent} + + + + + last seen {formatAiTrafficTimestamp(agent.lastSeenAt)} + +
+
+
+
+
+ + {agent.hits} hits + +
+
+ ); +} + +function BeaconSetup({ setup }: { setup: BeaconSetupResponse | undefined }) { + return ( +
+

+ No AI agent has been seen on your site yet. Install the beacon to start + recording hits. +

+
+        
+          {setup?.snippet ??
+            "Set BEACON_INGEST_SECRET to generate your install snippet"}
+        
+      
+ {setup?.token ? ( +

+ BEACON_ORG_TOKEN{" "} + {setup.token} +

+ ) : null} +
+ ); +} + +const PURPOSE_DONUT_CONFIG: ChartConfig = { + "Training data": { label: "Training data", color: "green" }, + "Search index": { label: "Search index", color: "blue" }, + "Used in answer": { label: "Used in answer", color: "purple" }, +}; + +function PurposeDonut({ agents }: { agents: AiTrafficAgent[] }) { + const rows = useMemo(() => { + const byPurpose = new Map(); + for (const agent of agents) { + const label = AI_TRAFFIC_PURPOSE_LABELS[agent.category] ?? agent.category; + byPurpose.set(label, (byPurpose.get(label) ?? 0) + agent.hits); + } + return [...byPurpose.entries()].map(([purpose, hits]) => ({ + purpose, + hits, + })); + }, [agents]); + + if (rows.length === 0) { + return null; + } + + return ( +
+

+ Requests by purpose +

+ + + + +
+ ); +} + +export function AiTrafficCard({ traffic, setup }: AiTrafficCardProps) { + const agents = traffic?.agents ?? []; + const maxHits = useMemo( + () => agents.reduce((max, agent) => Math.max(max, agent.hits), 0), + [agents] + ); + const totalHits = useMemo( + () => agents.reduce((sum, agent) => sum + agent.hits, 0), + [agents] + ); + + return ( + 0 + ? `${totalHits} requests · ${agents.length} agents · 30D` + : "crawlers and assistants fetching your pages" + } + > + {agents.length === 0 ? ( + + ) : ( +
+
+ {agents.map((agent) => ( + + ))} +
+ +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/ai-traffic-log-card.tsx b/apps/dashboard/src/components/geo/ai-traffic-log-card.tsx new file mode 100644 index 000000000..5ef9e8c83 --- /dev/null +++ b/apps/dashboard/src/components/geo/ai-traffic-log-card.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@notra/ui/components/ui/table"; +import { PurposeBadge } from "@/components/geo/purpose-badge"; +import { InstrumentModule } from "@/components/instrument/instrument-module"; +import type { AiTrafficLogCardProps, AiTrafficLogEntry } from "@/types/geo"; +import { formatAiTrafficTimestamp } from "@/utils/ai-traffic"; + +function LogRow({ entry }: { entry: AiTrafficLogEntry }) { + return ( + + + {formatAiTrafficTimestamp(entry.capturedAt)} + + {entry.agent} + + {entry.path} + + + + + + {entry.method} + + + ); +} + +export function AiTrafficLogCard({ log }: AiTrafficLogCardProps) { + return ( + +
+ + + + When + Provider + Path + Purpose + Method + + + + {log.map((entry) => ( + + ))} + +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/competitor-manager.tsx b/apps/dashboard/src/components/geo/competitor-manager.tsx new file mode 100644 index 000000000..ac6db32a3 --- /dev/null +++ b/apps/dashboard/src/components/geo/competitor-manager.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Badge } from "@notra/ui/components/ui/badge"; +import { Button } from "@notra/ui/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@notra/ui/components/ui/card"; +import { Input } from "@notra/ui/components/ui/input"; +import { Loader2Icon } from "lucide-react"; +import { useState } from "react"; +import { useGeoSettingsUpsert } from "@/lib/hooks/use-geo"; +import type { GeoSettings } from "@/types/geo"; + +interface CompetitorManagerProps { + organizationId: string; + settings: GeoSettings; +} + +export function CompetitorManager({ + organizationId, + settings, +}: CompetitorManagerProps) { + const [draft, setDraft] = useState(""); + const upsert = useGeoSettingsUpsert(organizationId); + + const save = (competitors: string[]) => { + upsert.mutate({ + organizationId, + companyName: settings.companyName, + aliases: settings.aliases, + competitors, + languages: settings.languages, + enabled: settings.enabled, + }); + }; + + const handleAdd = () => { + const name = draft.trim(); + if (name.length === 0 || settings.competitors.includes(name)) { + return; + } + save([...settings.competitors, name]); + setDraft(""); + }; + + return ( + + + Watched competitors + + Named competitors get called out in scans; unlisted brands are still + detected automatically. + + + +
+ setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + handleAdd(); + } + }} + placeholder="Competitor name" + value={draft} + /> + +
+
+ {settings.competitors.length === 0 && ( +

+ No competitors added yet +

+ )} + {settings.competitors.map((competitor) => ( + + {competitor} + + + ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/competitor-share-card.tsx b/apps/dashboard/src/components/geo/competitor-share-card.tsx new file mode 100644 index 000000000..b66e06b92 --- /dev/null +++ b/apps/dashboard/src/components/geo/competitor-share-card.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Bar } from "@notra/ui/components/dither-kit/bar"; +import { BarChart } from "@notra/ui/components/dither-kit/bar-chart"; +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +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 type { GeoCompetitorSharePoint } from "@/types/geo"; + +interface CompetitorShareCardProps { + points: GeoCompetitorSharePoint[]; + companyName: string | null; +} + +const chartConfig: ChartConfig = { + mentions: { label: "Mentions", color: "orange" }, +}; + +export function CompetitorShareCard({ + points, + companyName, +}: CompetitorShareCardProps) { + const rows = points.map((point) => ({ + brand: point.brand, + mentions: point.mentions, + })); + + return ( + + + Share of voice + + Brands AI engines bring up + {companyName ? ` alongside ${companyName}` : ""} + + + + {rows.length === 0 ? ( +

+ No competitor data yet +

+ ) : ( + + + + + + + + )} +
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/engine-radar-card.tsx b/apps/dashboard/src/components/geo/engine-radar-card.tsx new file mode 100644 index 000000000..8f29a3165 --- /dev/null +++ b/apps/dashboard/src/components/geo/engine-radar-card.tsx @@ -0,0 +1,101 @@ +"use client"; + +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +import { Radar } from "@notra/ui/components/dither-kit/radar"; +import { RadarChart } from "@notra/ui/components/dither-kit/radar-chart"; +import { Tooltip } from "@notra/ui/components/dither-kit/tooltip"; +import { useMemo, useState } from "react"; +import { ChartSeriesLegend } from "@/components/analytics/chart-legend"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { GEO_ENGINE_LABELS } from "@/constants/geo"; +import type { GeoOverviewEngine } from "@/types/geo"; +import { groupEngineFamilies } from "@/utils/geo-charts"; + +interface EngineRadarCardProps { + engines: GeoOverviewEngine[]; +} + +interface RadarRow { + engine: string; + web: number; + raw: number; +} + +const chartConfig: ChartConfig = { + web: { label: "With web search", color: "purple" }, + raw: { label: "Model only", color: "grey" }, +}; + +const seriesKeys = Object.keys(chartConfig); +const MIN_AXES = 3; +const WEB_LABEL_SUFFIX = /\s*\(web\)$/; +const PERCENT = 100; + +function familyLabel(family: string): string { + const label = + GEO_ENGINE_LABELS[family] ?? GEO_ENGINE_LABELS[`${family}-grounded`]; + if (!label) { + return family; + } + return label.replace(WEB_LABEL_SUFFIX, ""); +} + +export function EngineRadarCard({ engines }: EngineRadarCardProps) { + const [hiddenKeys, setHiddenKeys] = useState>(new Set()); + const rows = useMemo( + () => + groupEngineFamilies(engines).map((family) => ({ + engine: familyLabel(family.family), + web: Math.round((family.web?.mentionRate ?? 0) * PERCENT), + raw: Math.round((family.raw?.mentionRate ?? 0) * PERCENT), + })), + [engines] + ); + + const visibleKeys = seriesKeys.filter((key) => !hiddenKeys.has(key)); + + const toggle = (key: string) => { + setHiddenKeys((previous) => { + const next = new Set(previous); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + return ( + + {rows.length < MIN_AXES || visibleKeys.length === 0 ? ( + + ) : ( + + {visibleKeys.map((key) => ( + + ))} + `${value}%`} /> + + )} + + + ); +} diff --git a/apps/dashboard/src/components/geo/geo-settings-dialog.tsx b/apps/dashboard/src/components/geo/geo-settings-dialog.tsx new file mode 100644 index 000000000..ddc112215 --- /dev/null +++ b/apps/dashboard/src/components/geo/geo-settings-dialog.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { SUPPORTED_LANGUAGES } from "@notra/ai/constants/languages"; +import { + ResponsiveDialog, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, +} from "@notra/ui/components/shared/responsive-dialog"; +import { Button } from "@notra/ui/components/ui/button"; +import { Input } from "@notra/ui/components/ui/input"; +import { Label } from "@notra/ui/components/ui/label"; +import { Switch } from "@notra/ui/components/ui/switch"; +import { Loader2Icon } from "lucide-react"; +import { useEffect, useId, useState } from "react"; +import { GEO_LANGUAGE_FLAGS, GEO_MAX_LANGUAGES } from "@/constants/geo"; +import { useGeoSettingsUpsert } from "@/lib/hooks/use-geo"; +import { cn } from "@/lib/utils"; +import type { GeoSettings } from "@/types/geo"; + +interface GeoSettingsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + organizationId: string; + settings: GeoSettings | null; +} + +function parseList(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter((item) => item.length > 0); +} + +export function GeoSettingsDialog({ + open, + onOpenChange, + organizationId, + settings, +}: GeoSettingsDialogProps) { + const id = useId(); + const [companyName, setCompanyName] = useState(""); + const [aliases, setAliases] = useState(""); + const [competitors, setCompetitors] = useState(""); + const [languages, setLanguages] = useState([]); + const [enabled, setEnabled] = useState(true); + const upsert = useGeoSettingsUpsert(organizationId); + + useEffect(() => { + if (open) { + setCompanyName(settings?.companyName ?? ""); + setAliases(settings?.aliases.join(", ") ?? ""); + setCompetitors(settings?.competitors.join(", ") ?? ""); + setLanguages(settings?.languages ?? []); + setEnabled(settings?.enabled ?? true); + } + }, [open, settings]); + + const handleSave = () => { + upsert.mutate( + { + organizationId, + companyName: companyName.trim(), + aliases: parseList(aliases), + competitors: parseList(competitors), + languages, + enabled, + }, + { onSuccess: () => onOpenChange(false) } + ); + }; + + return ( + + + + GEO tracking settings + + What should AI engines be checked for? Aliases count as mentions + too. + + +
+
+ + setCompanyName(event.target.value)} + placeholder="Notra" + value={companyName} + /> +
+
+ + setAliases(event.target.value)} + placeholder="usenotra, notra.so (comma separated)" + value={aliases} + /> +
+
+ + setCompetitors(event.target.value)} + placeholder="Buffer, Hypefury (comma separated)" + value={competitors} + /> +
+
+ +
+ {SUPPORTED_LANGUAGES.filter( + (language) => language !== "English" + ).map((language) => { + const selected = languages.includes(language); + const atLimit = + !selected && languages.length >= GEO_MAX_LANGUAGES; + return ( + + ); + })} +
+

+ Each scan also asks the engines in these languages so you can see + how you perform beyond English. +

+
+
+ + +
+
+ + + +
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/geo-summary-stats.tsx b/apps/dashboard/src/components/geo/geo-summary-stats.tsx new file mode 100644 index 000000000..467500c8f --- /dev/null +++ b/apps/dashboard/src/components/geo/geo-summary-stats.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useMemo } from "react"; +import { InstrumentGrid } from "@/components/instrument/instrument-grid"; +import { GEO_ENGINE_LABELS } from "@/constants/geo"; +import { cn } from "@/lib/utils"; +import type { GeoStatTile, GeoSummaryStatsProps } from "@/types/geo"; +import { + buildGeoHeroSummary, + formatMentionRate, + gapInsight, +} from "@/utils/geo-charts"; + +export function GeoSummaryStats({ + engines, + settings, + promptCount, +}: GeoSummaryStatsProps) { + const { visibility, visibilityHint, tiles } = useMemo(() => { + const summary = buildGeoHeroSummary(engines); + const best = summary.bestEngine; + + const sideTiles: GeoStatTile[] = [ + { + label: "Best engine", + value: best ? (GEO_ENGINE_LABELS[best.engine] ?? best.engine) : "N/A", + hint: best + ? `${formatMentionRate(best.mentionRate)} mention rate` + : "run a scan", + }, + { + label: "Tracked prompts", + value: String(promptCount), + hint: "asked to every engine per scan", + }, + { + label: "Competitors watched", + value: String(settings.competitors.length), + hint: "named rivals in scans", + }, + ]; + + return { + visibility: + summary.visibilityRate === null + ? "N/A" + : formatMentionRate(summary.visibilityRate), + visibilityHint: gapInsight(summary.gapPoints), + tiles: sideTiles, + }; + }, [engines, settings.competitors.length, promptCount]); + + return ( + +
+

+ AI visibility +

+

+ {visibility} +

+

+ {visibilityHint} +

+
+ {tiles.map((tile, index) => ( +
+

+ {tile.label} +

+

+ {tile.value} +

+

{tile.hint}

+
+ ))} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/language-performance-card.tsx b/apps/dashboard/src/components/geo/language-performance-card.tsx new file mode 100644 index 000000000..fadd23b12 --- /dev/null +++ b/apps/dashboard/src/components/geo/language-performance-card.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { GEO_LANGUAGE_FLAGS } from "@/constants/geo"; +import { cn } from "@/lib/utils"; +import type { + GeoLanguageSharePoint, + LanguagePerformanceCardProps, +} from "@/types/geo"; +import { formatMentionRate } from "@/utils/geo-charts"; + +const PERCENT = 100; +const MIN_BAR_PERCENT = 2; + +function LanguageRow({ + point, + isBaseline, +}: { + point: GeoLanguageSharePoint; + isBaseline: boolean; +}) { + const percent = Math.round(point.mentionRate * PERCENT); + return ( +
+
+ + + + {point.language} + + + + {point.mentions}/{point.checks} checks + +
+
+
+
+
+ + {formatMentionRate(point.mentionRate)} + +
+
+ ); +} + +export function LanguagePerformanceCard({ + points, + configuredLanguages, +}: LanguagePerformanceCardProps) { + const hasExtraLanguages = + configuredLanguages.length > 0 || + points.some((point) => point.language !== "English"); + + return ( + + {hasExtraLanguages && points.length > 0 ? ( +
+ {points.map((point) => ( + + ))} +
+ ) : ( + + )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/mention-rate-card.tsx b/apps/dashboard/src/components/geo/mention-rate-card.tsx new file mode 100644 index 000000000..2f2a79d3d --- /dev/null +++ b/apps/dashboard/src/components/geo/mention-rate-card.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useMemo } from "react"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { GEO_ENGINE_LABELS } from "@/constants/geo"; +import { cn } from "@/lib/utils"; +import type { GeoOverviewEngine, MentionRateCardProps } from "@/types/geo"; +import { formatMentionRate } from "@/utils/geo-charts"; + +interface EngineFamily { + family: string; + label: string; + web: GeoOverviewEngine | null; + raw: GeoOverviewEngine | null; +} + +const GROUNDED_SUFFIX = /(-direct)?-grounded$/; +const WEB_LABEL_SUFFIX = /\s*\(web\)$/; + +function isGrounded(engine: string): boolean { + return GROUNDED_SUFFIX.test(engine) || engine === "perplexity-sonar"; +} + +function familyOf(engine: string): string { + return engine.replace(GROUNDED_SUFFIX, ""); +} + +function familyLabel(family: string): string { + const label = + GEO_ENGINE_LABELS[family] ?? GEO_ENGINE_LABELS[`${family}-grounded`]; + if (!label) { + return family; + } + return label.replace(WEB_LABEL_SUFFIX, ""); +} + +function groupEngines(engines: GeoOverviewEngine[]): EngineFamily[] { + const families = new Map(); + for (const engine of engines) { + const family = familyOf(engine.engine); + const entry = families.get(family) ?? { + family, + label: familyLabel(family), + web: null, + raw: null, + }; + if (isGrounded(engine.engine)) { + entry.web = engine; + } else { + entry.raw = engine; + } + families.set(family, entry); + } + return [...families.values()].sort( + (a, b) => + Math.max(b.web?.mentionRate ?? 0, b.raw?.mentionRate ?? 0) - + Math.max(a.web?.mentionRate ?? 0, a.raw?.mentionRate ?? 0) + ); +} + +function RateBar({ + variant, + engine, +}: { + variant: "web" | "raw"; + engine: GeoOverviewEngine | null; +}) { + if (!engine) { + return null; + } + const percent = Math.round(engine.mentionRate * 100); + return ( +
+ + {variant} + +
+
+
+ + + {formatMentionRate(engine.mentionRate)} + {" "} + + {engine.mentions}/{engine.checks} + + +
+ ); +} + +export function MentionRateCard({ engines }: MentionRateCardProps) { + const families = useMemo(() => groupEngines(engines), [engines]); + + return ( + + {families.length === 0 ? ( + + ) : ( +
+ {families.map((family) => ( +
+
+ {family.label} + {family.web?.avgPosition !== null && + family.web?.avgPosition !== undefined && ( + + avg position {family.web.avgPosition} + + )} +
+ + +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/mention-trend-card.tsx b/apps/dashboard/src/components/geo/mention-trend-card.tsx new file mode 100644 index 000000000..2012d2497 --- /dev/null +++ b/apps/dashboard/src/components/geo/mention-trend-card.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { Line } from "@notra/ui/components/dither-kit/area"; +import { LineChart } from "@notra/ui/components/dither-kit/area-chart"; +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; +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 { useMemo } from "react"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { ACCOUNT_SERIES_COLORS } from "@/constants/analytics"; +import { GEO_ENGINE_LABELS, GEO_TREND_MIN_DAYS } from "@/constants/geo"; +import type { MentionTrendCardProps } from "@/types/geo"; +import { buildMentionRateRows } from "@/utils/geo-charts"; + +export function MentionTrendCard({ + hero = false, + points, +}: MentionTrendCardProps) { + const { rows, engines } = useMemo( + () => buildMentionRateRows(points), + [points] + ); + + const config = useMemo(() => { + const trendConfig: ChartConfig = {}; + engines.forEach((engine, index) => { + trendConfig[engine] = { + label: GEO_ENGINE_LABELS[engine] ?? engine, + color: + ACCOUNT_SERIES_COLORS[index % ACCOUNT_SERIES_COLORS.length] ?? + "purple", + }; + }); + return trendConfig; + }, [engines]); + + return ( + + {rows.length < GEO_TREND_MIN_DAYS ? ( + + ) : ( + + + + + {engines.map((engine) => ( + + ))} + `${value}%`} /> + + )} + + ); +} diff --git a/apps/dashboard/src/components/geo/model-usage-card.tsx b/apps/dashboard/src/components/geo/model-usage-card.tsx new file mode 100644 index 000000000..7e5ac235c --- /dev/null +++ b/apps/dashboard/src/components/geo/model-usage-card.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useMemo } from "react"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { cn } from "@/lib/utils"; +import type { GeoModelUsageResponse, GeoModelUsageRow } from "@/types/geo"; +import { + formatMentionRate, + formatUsageShare, + usageBarWidth, +} from "@/utils/geo-charts"; + +interface ModelUsageCardProps { + usage: GeoModelUsageResponse | undefined; +} + +function UsageRow({ + model, + maxShare, +}: { + model: GeoModelUsageRow; + maxShare: number; +}) { + return ( +
+
+ + {model.label} + + + {model.scanned && model.mentionRate !== null ? ( + + {formatMentionRate(model.mentionRate)} mention rate + + ) : ( + not scanned + )} + +
+
+
+
+
+ + {formatUsageShare(model.share)} + +
+
+ ); +} + +export function ModelUsageCard({ usage }: ModelUsageCardProps) { + const models = usage?.models ?? []; + const maxShare = useMemo( + () => models.reduce((max, model) => Math.max(max, model.share), 0), + [models] + ); + const scannedCount = useMemo( + () => models.filter((model) => model.scanned).length, + [models] + ); + + return ( + 0 + ? `we scan ${scannedCount} of the top ${models.length} · ${usage?.attribution ?? ""}` + : "industry token share per model" + } + > + {models.length === 0 ? ( + + ) : ( +
+ {models.map((model) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/presence-badge.tsx b/apps/dashboard/src/components/geo/presence-badge.tsx new file mode 100644 index 000000000..99e19e1cd --- /dev/null +++ b/apps/dashboard/src/components/geo/presence-badge.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Badge } from "@notra/ui/components/ui/badge"; +import { GEO_PRESENCE_DOT_CLASSES, GEO_PRESENCE_LABELS } from "@/constants/geo"; +import { cn } from "@/lib/utils"; +import type { GeoPresenceStatus } from "@/types/geo"; + +interface PresenceBadgeProps { + status: GeoPresenceStatus | null; +} + +const PRESENCE_TITLES: Record = { + "training-data": + "Mentioned even without web access: you're in the model's training data", + "retrieval-only": + "Only mentioned when engines search the web: indexed, not memorized", + invisible: "No engine mentions you on this prompt yet", +}; + +export function PresenceBadge({ status }: PresenceBadgeProps) { + if (!status) { + return null; + } + return ( + + + {GEO_PRESENCE_LABELS[status]} + + ); +} diff --git a/apps/dashboard/src/components/geo/prompt-manager.tsx b/apps/dashboard/src/components/geo/prompt-manager.tsx new file mode 100644 index 000000000..11d772cf6 --- /dev/null +++ b/apps/dashboard/src/components/geo/prompt-manager.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Badge } from "@notra/ui/components/ui/badge"; +import { Button } from "@notra/ui/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@notra/ui/components/ui/card"; +import { Input } from "@notra/ui/components/ui/input"; +import { Switch } from "@notra/ui/components/ui/switch"; +import { Loader2Icon } from "lucide-react"; +import { useState } from "react"; +import { GEO_LANGUAGE_FLAGS } from "@/constants/geo"; +import { + useGeoPromptCreate, + useGeoPromptDelete, + useGeoPrompts, + useGeoPromptToggle, + useGeoSettings, +} from "@/lib/hooks/use-geo"; +import type { GeoTrackedPrompt } from "@/types/geo"; + +interface PromptManagerProps { + organizationId: string; +} + +const MIN_PROMPT_LENGTH = 8; + +function PromptRow({ + prompt, + organizationId, + languages, +}: { + prompt: GeoTrackedPrompt; + organizationId: string; + languages: string[]; +}) { + const toggle = useGeoPromptToggle(organizationId); + const remove = useGeoPromptDelete(organizationId); + + return ( +
+

{prompt.prompt}

+ {languages.length > 0 && ( + + {languages.map((language) => ( + + {GEO_LANGUAGE_FLAGS[language]} + + ))} + + )} + {prompt.source === "auto" ? ( + + Auto + + ) : ( +
+ + toggle.mutate({ promptId: prompt.id, enabled }) + } + /> + +
+ )} +
+ ); +} + +export function PromptManager({ organizationId }: PromptManagerProps) { + const { data: settingsData } = useGeoSettings(organizationId); + const languages = settingsData?.settings?.languages ?? []; + const [draft, setDraft] = useState(""); + const { data } = useGeoPrompts(organizationId); + const create = useGeoPromptCreate(organizationId); + + const prompts = data?.prompts ?? []; + const customPrompts = prompts.filter((prompt) => prompt.source === "custom"); + const autoPrompts = prompts.filter((prompt) => prompt.source === "auto"); + + const handleAdd = () => { + const prompt = draft.trim(); + if (prompt.length < MIN_PROMPT_LENGTH) { + return; + } + create.mutate({ prompt }, { onSuccess: () => setDraft("") }); + }; + + return ( + + + Tracked prompts + + Every scan asks each engine these questions. Add the questions your + customers actually ask. + + + +
+ setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + handleAdd(); + } + }} + placeholder="What's the best tool for automating changelogs?" + value={draft} + /> + +
+
+ {customPrompts.map((prompt) => ( + + ))} + {autoPrompts.map((prompt) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/prompt-results-card.tsx b/apps/dashboard/src/components/geo/prompt-results-card.tsx new file mode 100644 index 000000000..9763781c9 --- /dev/null +++ b/apps/dashboard/src/components/geo/prompt-results-card.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { Badge } from "@notra/ui/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@notra/ui/components/ui/card"; +import { useMemo } from "react"; +import { PresenceBadge } from "@/components/geo/presence-badge"; +import { GEO_ENGINE_LABELS } from "@/constants/geo"; +import type { GeoPromptResult } from "@/types/geo"; +import { classifyPromptPresence } from "@/utils/geo-presence"; + +interface PromptResultsCardProps { + results: GeoPromptResult[]; +} + +interface PromptGroup { + promptId: string; + prompt: string; + results: GeoPromptResult[]; +} + +function groupByPrompt(results: GeoPromptResult[]): PromptGroup[] { + const groups = new Map(); + for (const result of results) { + const group = groups.get(result.promptId) ?? { + promptId: result.promptId, + prompt: result.prompt, + results: [], + }; + group.results.push(result); + groups.set(result.promptId, group); + } + return [...groups.values()]; +} + +function ResultBadge({ result }: { result: GeoPromptResult }) { + const label = GEO_ENGINE_LABELS[result.engine] ?? result.engine; + if (!result.mentioned) { + return ( + + {label} · not mentioned + + ); + } + return ( + + {label} + {result.position !== null && ` · #${result.position}`} + {result.sentiment === "positive" && " · positive"} + {result.sentiment === "negative" && " · negative"} + + ); +} + +export function PromptResultsCard({ results }: PromptResultsCardProps) { + const groups = useMemo(() => groupByPrompt(results), [results]); + + return ( + + + Prompt results + + Latest answer per prompt across every engine + + + + {groups.length === 0 ? ( +

+ Run a scan to see how AI engines answer these prompts +

+ ) : ( +
+ {groups.map((group) => { + const mentionedResult = group.results.find( + (result) => result.mentioned && result.excerpt + ); + return ( +
+
+

{group.prompt}

+ +
+
+ {group.results.map((result) => ( + + ))} +
+ {mentionedResult && ( +

+ "{mentionedResult.excerpt}" +

+ )} +
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/apps/dashboard/src/components/geo/prompt-results-preview.tsx b/apps/dashboard/src/components/geo/prompt-results-preview.tsx new file mode 100644 index 000000000..fd164c43f --- /dev/null +++ b/apps/dashboard/src/components/geo/prompt-results-preview.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { Badge } from "@notra/ui/components/ui/badge"; +import type { ReactNode } from "react"; +import { useMemo } from "react"; +import { PresenceBadge } from "@/components/geo/presence-badge"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { GEO_LANGUAGE_FLAGS } from "@/constants/geo"; +import type { GeoPresenceStatus, GeoPromptResult } from "@/types/geo"; +import { classifyPromptPresence } from "@/utils/geo-presence"; + +interface PromptResultsPreviewProps { + results: GeoPromptResult[]; + limit?: number; + action?: ReactNode; + languages?: string[]; +} + +interface PromptSummary { + promptId: string; + prompt: string; + mentioned: number; + total: number; + bestPosition: number | null; + presence: GeoPresenceStatus | null; + results: GeoPromptResult[]; +} + +const DEFAULT_LIMIT = 3; + +function summarize(results: GeoPromptResult[]): PromptSummary[] { + const groups = new Map(); + for (const result of results) { + const group = groups.get(result.promptId) ?? { + promptId: result.promptId, + prompt: result.prompt, + mentioned: 0, + total: 0, + bestPosition: null, + presence: null, + results: [], + }; + group.results.push(result); + group.total += 1; + if (result.mentioned) { + group.mentioned += 1; + } + if ( + result.position !== null && + (group.bestPosition === null || result.position < group.bestPosition) + ) { + group.bestPosition = result.position; + } + groups.set(result.promptId, group); + } + const summaries = [...groups.values()].map((group) => ({ + ...group, + presence: classifyPromptPresence(group.results), + })); + return summaries.sort( + (a, b) => b.mentioned / b.total - a.mentioned / a.total + ); +} + +export function PromptResultsPreview({ + results, + limit = DEFAULT_LIMIT, + action, + languages = [], +}: PromptResultsPreviewProps) { + const summaries = useMemo(() => summarize(results), [results]); + + return ( + + {summaries.length === 0 ? ( + + ) : ( +
+ {summaries.slice(0, limit).map((summary) => ( +
+

+ {summary.prompt} +

+ {languages.length > 0 && ( + + {languages.map((language) => ( + + {GEO_LANGUAGE_FLAGS[language]} + + ))} + + )} + + {summary.bestPosition !== null && ( + + #{summary.bestPosition} + + )} + + {summary.mentioned}/{summary.total} engines + +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/purpose-badge.tsx b/apps/dashboard/src/components/geo/purpose-badge.tsx new file mode 100644 index 000000000..f90a0209b --- /dev/null +++ b/apps/dashboard/src/components/geo/purpose-badge.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Badge } from "@notra/ui/components/ui/badge"; +import { + AI_TRAFFIC_PURPOSE_DESCRIPTIONS, + AI_TRAFFIC_PURPOSE_LABELS, +} from "@/constants/geo"; + +interface PurposeBadgeProps { + category: string; +} + +export function PurposeBadge({ category }: PurposeBadgeProps) { + return ( + + {AI_TRAFFIC_PURPOSE_LABELS[category] ?? category} + + ); +} diff --git a/apps/dashboard/src/components/geo/share-of-voice-donut.tsx b/apps/dashboard/src/components/geo/share-of-voice-donut.tsx new file mode 100644 index 000000000..a31cf3d3d --- /dev/null +++ b/apps/dashboard/src/components/geo/share-of-voice-donut.tsx @@ -0,0 +1,130 @@ +"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 type { ReactNode } from "react"; +import { useMemo } from "react"; +import { + InstrumentEmpty, + InstrumentModule, +} from "@/components/instrument/instrument-module"; +import { ACCOUNT_SERIES_COLORS } from "@/constants/analytics"; +import type { GeoCompetitorSharePoint } from "@/types/geo"; + +interface ShareOfVoiceDonutProps { + points: GeoCompetitorSharePoint[]; + action?: ReactNode; +} + +interface SliceRow { + brand: string; + mentions: number; +} + +const TOP_SLICES = 5; +const PERCENT = 100; +const DONUT_INNER_RADIUS = 0.55; + +export function ShareOfVoiceDonut({ points, action }: ShareOfVoiceDonutProps) { + const { rows, config, total, caption } = useMemo(() => { + const top = points.slice(0, TOP_SLICES); + const rest = points.slice(TOP_SLICES); + const sliceRows: SliceRow[] = top.map((point) => ({ + brand: point.brand, + mentions: point.mentions, + })); + const otherTotal = rest.reduce((sum, point) => sum + point.mentions, 0); + if (otherTotal > 0) { + sliceRows.push({ brand: "Other", mentions: otherTotal }); + } + const sliceConfig: ChartConfig = {}; + sliceRows.forEach((row, index) => { + sliceConfig[row.brand] = { + label: row.brand, + color: + row.brand === "Other" + ? "grey" + : (ACCOUNT_SERIES_COLORS[index % ACCOUNT_SERIES_COLORS.length] ?? + "blue"), + }; + }); + const sliceTotal = sliceRows.reduce((sum, row) => sum + row.mentions, 0); + const topSlice = sliceRows.reduce( + (best, row) => + row.brand !== "Other" && (best === null || row.mentions > best.mentions) + ? row + : best, + null + ); + return { + rows: sliceRows, + config: sliceConfig, + total: sliceTotal, + caption: + topSlice && sliceTotal > 0 + ? `${topSlice.brand} · ${Math.round((topSlice.mentions / sliceTotal) * PERCENT)}% of mentions` + : null, + }; + }, [points]); + + return ( + + {rows.length === 0 ? ( + + ) : ( +
+
+ + + + +
+ {rows.map((row) => ( +
+ + + {row.brand} + + + {total > 0 + ? `${Math.round((row.mentions / total) * PERCENT)}%` + : "0%"} + +
+ ))} +
+
+ {caption && ( +

+ {caption} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/geo/website-generate-card.tsx b/apps/dashboard/src/components/geo/website-generate-card.tsx new file mode 100644 index 000000000..f0ab797cd --- /dev/null +++ b/apps/dashboard/src/components/geo/website-generate-card.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Button } from "@notra/ui/components/ui/button"; +import { Card, CardContent } from "@notra/ui/components/ui/card"; +import { Input } from "@notra/ui/components/ui/input"; +import { Label } from "@notra/ui/components/ui/label"; +import { Loader2Icon } from "lucide-react"; +import { useId, useState } from "react"; +import { useGeoGenerateFromWebsite } from "@/lib/hooks/use-geo"; + +interface WebsiteGenerateCardProps { + organizationId: string; + compact?: boolean; +} + +const URL_PREFIX = /^https?:\/\//i; + +export function WebsiteGenerateCard({ + organizationId, + compact = false, +}: WebsiteGenerateCardProps) { + const id = useId(); + const [url, setUrl] = useState(""); + const generate = useGeoGenerateFromWebsite(organizationId); + + const handleGenerate = () => { + const trimmed = url.trim(); + if (trimmed.length === 0) { + return; + } + const normalized = URL_PREFIX.test(trimmed) + ? trimmed + : `https://${trimmed}`; + generate.mutate({ url: normalized }); + }; + + const form = ( +
+ setUrl(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + handleGenerate(); + } + }} + placeholder="yourcompany.com" + value={url} + /> + +
+ ); + + if (compact) { + return ( +
+ + {form} +
+ ); + } + + return ( + + +
+

Set up from your website

+

+ We read your site, work out your company, aliases, and competitors, + and write the prompts your buyers actually ask AI assistants. +

+
+ {form} +
+
+ ); +} 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 new file mode 100644 index 000000000..b42ca2707 --- /dev/null +++ b/apps/dashboard/src/constants/analytics.ts @@ -0,0 +1,22 @@ +import type { DitherColor } from "@notra/ui/components/dither-kit/palette"; + +export const PROVIDER_DITHER_COLORS: Record = { + twitter: "grey", + linkedin: "blue", +}; + +export const ACCOUNT_SERIES_COLORS: DitherColor[] = [ + "blue", + "purple", + "green", + "orange", + "pink", + "red", + "grey", +]; + +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/constants/experiments.ts b/apps/dashboard/src/constants/experiments.ts new file mode 100644 index 000000000..2e925b085 --- /dev/null +++ b/apps/dashboard/src/constants/experiments.ts @@ -0,0 +1,14 @@ +export const EXPERIMENT_METRICS = [ + "engagement", + "impressions", + "likes", +] as const; + +export const EXPERIMENT_METRIC_LABELS: Record = { + engagement: "Engagement", + impressions: "Impressions", + likes: "Likes", +}; + +export const EXPERIMENT_WINNER_LIFT_THRESHOLD = 0.05; +export const EXPERIMENT_POST_PICKER_LIMIT = 30; diff --git a/apps/dashboard/src/constants/geo.ts b/apps/dashboard/src/constants/geo.ts new file mode 100644 index 000000000..3f0b0a1f8 --- /dev/null +++ b/apps/dashboard/src/constants/geo.ts @@ -0,0 +1,172 @@ +import { LANGUAGE_FLAGS } from "@/constants/brand-identity"; +import type { GeoGroundedEngine } from "@/types/geo"; + +export const GEO_ENGINES = [ + "openai/gpt-5.4", + "anthropic/claude-sonnet-4.6", + "google/gemini-3-flash", + "anthropic/claude-opus-5", + "anthropic/claude-haiku-4.5", + "openai/gpt-5.4-mini", +] as const; + +export const GEO_JUDGE_MODEL = "openai/gpt-5.4-nano"; + +export const GEO_OPENAI_API_KEY_ENV = "OPENAI_API_KEY"; +export const GEO_ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"; +export const GEO_PERPLEXITY_API_KEY_ENV = "PERPLEXITY_API_KEY"; + +const hasEnv = (name: string): boolean => { + const value = process.env[name]; + return typeof value === "string" && value.length > 0; +}; + +export const GEO_GROUNDED_ENGINES: readonly GeoGroundedEngine[] = [ + { + key: "openai/gpt-5.4-grounded", + label: "ChatGPT (web)", + model: "openai/gpt-5.4", + provider: "gateway-openai", + envVar: null, + isAvailable: () => true, + }, + { + key: "anthropic/claude-sonnet-4.6-grounded", + label: "Claude (web)", + model: "anthropic/claude-sonnet-4.6", + provider: "gateway-anthropic", + envVar: null, + isAvailable: () => true, + }, + { + key: "google/gemini-3-flash-grounded", + label: "Gemini (web)", + model: "google/gemini-3-flash", + provider: "gateway-google", + envVar: null, + isAvailable: () => true, + }, + { + key: "openai-direct-grounded", + label: "ChatGPT (web)", + model: "gpt-5.4", + provider: "direct-openai", + envVar: GEO_OPENAI_API_KEY_ENV, + isAvailable: () => hasEnv(GEO_OPENAI_API_KEY_ENV), + }, + { + key: "anthropic-direct-grounded", + label: "Claude (web)", + model: "claude-sonnet-4-6", + provider: "direct-anthropic", + envVar: GEO_ANTHROPIC_API_KEY_ENV, + isAvailable: () => hasEnv(GEO_ANTHROPIC_API_KEY_ENV), + }, + { + key: "perplexity-sonar", + label: "Perplexity", + model: "sonar", + provider: "direct-perplexity", + envVar: GEO_PERPLEXITY_API_KEY_ENV, + isAvailable: () => hasEnv(GEO_PERPLEXITY_API_KEY_ENV), + }, +]; + +const groundedEngineLabels = Object.fromEntries( + GEO_GROUNDED_ENGINES.map((engine) => [engine.key, engine.label]) +); + +export const GEO_ENGINE_LABELS: Record = { + "openai/gpt-5.4": "ChatGPT", + "anthropic/claude-sonnet-4.6": "Claude", + "google/gemini-3-flash": "Gemini", + "anthropic/claude-opus-5": "Claude Opus", + "anthropic/claude-haiku-4.5": "Claude Haiku", + "openai/gpt-5.4-mini": "GPT-5.4 mini", + ...groundedEngineLabels, +}; + +export const GEO_MODEL_USAGE_SOURCE = "openrouter"; +export const GEO_MODEL_USAGE_ATTRIBUTION = + "Source: OpenRouter (openrouter.ai/rankings)"; +export const GEO_MODEL_USAGE_API_KEY_ENV = "OPENROUTER_API_KEY"; +export const GEO_MODEL_USAGE_ENDPOINT = + "https://openrouter.ai/api/v1/datasets/rankings-daily"; +export const GEO_MODEL_USAGE_PERIOD = "week"; +export const GEO_MODEL_USAGE_OTHER_KEY = "other"; +export const GEO_MODEL_USAGE_MODELS_ENDPOINT = + "https://openrouter.ai/api/v1/models"; +export const GEO_MODEL_USAGE_INGEST_LIMIT = 40; +export const GEO_MODEL_USAGE_FETCH_TIMEOUT_MS = 20_000; +export const GEO_MODEL_USAGE_DEFAULT_LIMIT = 12; +export const GEO_MODEL_USAGE_DEFAULT_WEEKS = 8; + +export const GEO_MAX_PROMPTS = 8; +export const GEO_GROUNDED_MAX_PROMPTS = 6; +export const GEO_GROUNDED_MAX_SEARCHES = 3; +export const GEO_ANSWER_MAX_TOKENS = 600; +export const GEO_GROUNDED_ANSWER_MAX_TOKENS = 1200; +export const GEO_JUDGE_MAX_TOKENS = 800; +export const GEO_SCAN_CONCURRENCY = 4; +export const GEO_EXCERPT_MAX_LENGTH = 300; +export const GEO_PROMPT_MIN_LENGTH = 8; +export const GEO_PROMPT_MAX_LENGTH = 300; +export const GEO_DISCOVERY_MODEL = "anthropic/claude-sonnet-4.6"; +export const GEO_DISCOVERY_MAX_TOKENS = 4000; +export const GEO_DISCOVERY_MAX_ALIASES = 6; +export const GEO_DISCOVERY_MIN_COMPETITORS = 5; +export const GEO_DISCOVERY_MAX_COMPETITORS = 10; +export const GEO_DISCOVERY_MIN_PROMPTS = 10; +export const GEO_DISCOVERY_MAX_PROMPTS = 14; +export const GEO_DISCOVERY_MAX_BRANDED_PROMPTS = 3; +export const GEO_DISCOVERY_ALIAS_LIMIT = 8; +export const GEO_DISCOVERY_COMPETITOR_LIMIT = 12; +export const GEO_DISCOVERY_SYSTEM_PROMPT = + "You are a search visibility analyst. You read a company's website and derive the brand identity and the buyer questions that decide whether an AI assistant recommends this company. Respond only with the requested structured data."; +export const GEO_ANSWER_SYSTEM_PROMPT = + "You are a helpful AI assistant. Answer the user's question directly and concretely, naming specific products or companies where relevant."; + +export const AI_TRAFFIC_DEFAULT_DAYS = 30; +export const AI_TRAFFIC_DEFAULT_LOG_LIMIT = 50; +export const BEACON_INGEST_PATH = "/api/beacon"; +export const BEACON_INGEST_SECRET_ENV = "BEACON_INGEST_SECRET"; + +export const AI_TRAFFIC_PURPOSE_LABELS: Record = { + "training-crawler": "Training data", + "search-index": "Search index", + "assistant-browse": "Used in answer", +}; + +export const AI_TRAFFIC_PURPOSE_DESCRIPTIONS: Record = { + "training-crawler": "Collects pages for model training corpora", + "search-index": "Builds the index an AI answer engine searches", + "assistant-browse": + "Fetched while an assistant was answering someone. A fetch is not proof of a citation", +}; + +export const AI_TRAFFIC_CONFIDENCE_LABELS: Record = { + verified: "Verified", + reported: "Reported", + heuristic: "Heuristic", +}; + +export const GEO_PRESENCE_LABELS: Record = { + "training-data": "Training data", + "retrieval-only": "Retrieval only", + invisible: "Invisible", +}; + +export const GEO_PRESENCE_DOT_CLASSES: Record = { + "training-data": "bg-emerald-500", + "retrieval-only": "bg-amber-500", + invisible: "bg-muted-foreground/50", +}; + +export const GEO_TREND_MIN_DAYS = 5; + +export const GEO_MAX_LANGUAGES = 3; +export const GEO_LANGUAGE_MAX_PROMPTS = 5; +export const GEO_LANGUAGE_GROUNDED_MAX_PROMPTS = 3; +export const GEO_TRANSLATION_MAX_TOKENS = 2000; + +export const GEO_LANGUAGE_FLAGS: Record = LANGUAGE_FLAGS; diff --git a/apps/dashboard/src/lib/analytics/constants.ts b/apps/dashboard/src/lib/analytics/constants.ts new file mode 100644 index 000000000..5c0ef09d2 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/constants.ts @@ -0,0 +1,6 @@ +export const TWITTER_USER_BATCH_SIZE = 100; +export const TWITTER_TIMELINE_MAX_RESULTS = 100; +export const TWITTER_TIMELINE_MAX_PAGES = 5; +export const TWITTER_USER_FIELDS = + "public_metrics,verified,verified_type,profile_image_url,name"; +export const TWITTER_TWEET_FIELDS = "text,created_at,public_metrics"; 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/record-post.ts b/apps/dashboard/src/lib/analytics/record-post.ts new file mode 100644 index 000000000..9d22e87c5 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/record-post.ts @@ -0,0 +1,43 @@ +import { + ingestSocialPostSources, + ingestSocialPosts, + isTinybirdConfigured, +} from "@notra/analytics/tinybird/client"; +import { toClickHouseDateTime } from "@notra/analytics/utils/datetime"; +import type { RecordPublishedSocialPostInput } from "@/types/analytics"; + +export async function recordPublishedSocialPost( + input: RecordPublishedSocialPostInput +): Promise { + if (!isTinybirdConfigured()) { + return; + } + const now = toClickHouseDateTime(new Date()); + try { + await ingestSocialPosts([ + { + organization_id: input.organizationId, + account_id: input.accountId, + provider: input.provider, + provider_account_id: input.providerAccountId, + platform_post_id: input.platformPostId, + url: input.url, + content: input.content, + posted_at: now, + captured_at: now, + }, + ]); + await ingestSocialPostSources([ + { + organization_id: input.organizationId, + provider: input.provider, + provider_account_id: input.providerAccountId, + platform_post_id: input.platformPostId, + source: "notra", + captured_at: now, + }, + ]); + } catch (error) { + console.error("[Analytics] Failed to record published post:", error); + } +} diff --git a/apps/dashboard/src/lib/analytics/rows.ts b/apps/dashboard/src/lib/analytics/rows.ts new file mode 100644 index 000000000..783083d7c --- /dev/null +++ b/apps/dashboard/src/lib/analytics/rows.ts @@ -0,0 +1,91 @@ +import type { + SocialAccountRow, + SocialAccountStatsRow, + SocialPostRow, + SocialPostStatsRow, +} from "@notra/analytics/tinybird/datasources"; +import { toClickHouseDateTime } from "@notra/analytics/utils/datetime"; +import type { + SyncableSocialAccount, + TwitterBatchUser, + TwitterTimelineTweet, +} from "@/types/analytics"; + +export function buildAccountRow( + account: SyncableSocialAccount, + capturedAt: Date +): SocialAccountRow { + return { + organization_id: account.organizationId, + account_id: account.id, + provider: account.provider, + provider_account_id: account.providerAccountId, + username: account.username, + display_name: account.displayName, + profile_image_url: account.profileImageUrl, + profile_url: null, + account_type: null, + verified: account.verified, + captured_at: toClickHouseDateTime(capturedAt), + }; +} + +export function buildTwitterAccountStatsRow( + account: SyncableSocialAccount, + user: TwitterBatchUser, + capturedAt: Date +): SocialAccountStatsRow { + const metrics = user.public_metrics; + return { + organization_id: account.organizationId, + account_id: account.id, + provider: account.provider, + provider_account_id: account.providerAccountId, + captured_at: toClickHouseDateTime(capturedAt), + followers_count: metrics?.followers_count ?? null, + following_count: metrics?.following_count ?? null, + posts_count: metrics?.tweet_count ?? null, + listed_count: metrics?.listed_count ?? null, + }; +} + +export function buildTweetPostRow( + account: SyncableSocialAccount, + tweet: TwitterTimelineTweet, + capturedAt: Date +): SocialPostRow { + return { + organization_id: account.organizationId, + account_id: account.id, + provider: account.provider, + provider_account_id: account.providerAccountId, + platform_post_id: tweet.id, + url: `https://x.com/${account.username}/status/${tweet.id}`, + content: tweet.text, + posted_at: toClickHouseDateTime( + tweet.created_at ? new Date(tweet.created_at) : capturedAt + ), + captured_at: toClickHouseDateTime(capturedAt), + }; +} + +export function buildTweetStatsRow( + account: SyncableSocialAccount, + tweet: TwitterTimelineTweet, + capturedAt: Date +): SocialPostStatsRow { + const metrics = tweet.public_metrics; + return { + organization_id: account.organizationId, + provider: account.provider, + provider_account_id: account.providerAccountId, + platform_post_id: tweet.id, + captured_at: toClickHouseDateTime(capturedAt), + impressions: metrics?.impression_count ?? null, + likes: metrics?.like_count ?? null, + replies: metrics?.reply_count ?? null, + reposts: metrics?.retweet_count ?? null, + quotes: metrics?.quote_count ?? null, + bookmarks: metrics?.bookmark_count ?? null, + }; +} 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/analytics/twitter-sync.ts b/apps/dashboard/src/lib/analytics/twitter-sync.ts new file mode 100644 index 000000000..1b75a14f7 --- /dev/null +++ b/apps/dashboard/src/lib/analytics/twitter-sync.ts @@ -0,0 +1,124 @@ +import type { + SocialAccountStatsRow, + SocialPostRow, + SocialPostStatsRow, +} from "@notra/analytics/tinybird/datasources"; +import { + TWITTER_TIMELINE_MAX_PAGES, + TWITTER_TIMELINE_MAX_RESULTS, + TWITTER_TWEET_FIELDS, + TWITTER_USER_BATCH_SIZE, + TWITTER_USER_FIELDS, +} from "@/lib/analytics/constants"; +import { + buildTweetPostRow, + buildTweetStatsRow, + buildTwitterAccountStatsRow, +} from "@/lib/analytics/rows"; +import type { + SyncableSocialAccount, + TwitterBatchUser, + TwitterBatchUsersResponse, + TwitterTimelineResponse, +} from "@/types/analytics"; +import { twitterAppFetch } from "@/utils/twitter-fetcher"; + +function isTwitterAnalyticsConfigured(): boolean { + return Boolean(process.env.TWITTER_BEARER_TOKEN); +} + +async function fetchTwitterUsersBatch( + usernames: string[] +): Promise { + const users: TwitterBatchUser[] = []; + for ( + let index = 0; + index < usernames.length; + index += TWITTER_USER_BATCH_SIZE + ) { + const batch = usernames.slice(index, index + TWITTER_USER_BATCH_SIZE); + const params = new URLSearchParams({ + usernames: batch.join(","), + "user.fields": TWITTER_USER_FIELDS, + }); + const response = await twitterAppFetch( + `https://api.x.com/2/users/by?${params.toString()}` + ); + if (!response.ok) { + continue; + } + const json: TwitterBatchUsersResponse = await response.json(); + users.push(...(json.data ?? [])); + } + return users; +} + +async function fetchUserTweets(userId: string) { + const tweets: TwitterTimelineResponse["data"] = []; + let paginationToken: string | undefined; + + for (let page = 0; page < TWITTER_TIMELINE_MAX_PAGES; page += 1) { + const params = new URLSearchParams({ + max_results: String(TWITTER_TIMELINE_MAX_RESULTS), + exclude: "replies,retweets", + "tweet.fields": TWITTER_TWEET_FIELDS, + }); + if (paginationToken) { + params.set("pagination_token", paginationToken); + } + const response = await twitterAppFetch( + `https://api.x.com/2/users/${encodeURIComponent(userId)}/tweets?${params.toString()}` + ); + if (!response.ok) { + break; + } + const json: TwitterTimelineResponse = await response.json(); + tweets.push(...(json.data ?? [])); + paginationToken = json.meta?.next_token; + if (!paginationToken) { + break; + } + } + + return tweets; +} + +export interface TwitterSyncRows { + accountStats: SocialAccountStatsRow[]; + posts: SocialPostRow[]; + postStats: SocialPostStatsRow[]; +} + +export async function collectTwitterRows( + accounts: SyncableSocialAccount[], + capturedAt: Date +): Promise { + const rows: TwitterSyncRows = { accountStats: [], posts: [], postStats: [] }; + if (accounts.length === 0 || !isTwitterAnalyticsConfigured()) { + return rows; + } + + const users = await fetchTwitterUsersBatch( + accounts.map((account) => account.username) + ); + const usersByUsername = new Map( + users.map((user) => [user.username.toLowerCase(), user]) + ); + + for (const account of accounts) { + const user = usersByUsername.get(account.username.toLowerCase()); + if (!user) { + continue; + } + rows.accountStats.push( + buildTwitterAccountStatsRow(account, user, capturedAt) + ); + const tweets = await fetchUserTweets(user.id); + for (const tweet of tweets) { + rows.posts.push(buildTweetPostRow(account, tweet, capturedAt)); + rows.postStats.push(buildTweetStatsRow(account, tweet, capturedAt)); + } + } + + return rows; +} diff --git a/apps/dashboard/src/lib/beacon/snippet.ts b/apps/dashboard/src/lib/beacon/snippet.ts new file mode 100644 index 000000000..c8336b5ed --- /dev/null +++ b/apps/dashboard/src/lib/beacon/snippet.ts @@ -0,0 +1,33 @@ +import { BEACON_INGEST_PATH } from "@/constants/geo"; + +const FALLBACK_APP_URL = "https://app.usenotra.com"; + +export function buildBeaconIngestUrl(): string { + const base = + process.env.NEXT_PUBLIC_APP_URL ?? + process.env.BETTER_AUTH_URL ?? + FALLBACK_APP_URL; + return new URL(BEACON_INGEST_PATH, base).toString(); +} + +export function buildBeaconSnippet( + ingestUrl: string, + organizationId: string +): string { + return [ + "// middleware.ts (proxy.ts on Next.js 16)", + 'import { createBeaconMiddleware } from "@notra/beacon/middleware";', + 'import { NextResponse } from "next/server";', + "", + "const beacon = createBeaconMiddleware({", + ` ingestUrl: "${ingestUrl}",`, + ' token: process.env.BEACON_ORG_TOKEN ?? "",', + ` organizationId: "${organizationId}",`, + "});", + "", + "export function middleware(request: Request) {", + " beacon(request);", + " return NextResponse.next();", + "}", + ].join("\n"); +} diff --git a/apps/dashboard/src/lib/beacon/token.ts b/apps/dashboard/src/lib/beacon/token.ts new file mode 100644 index 000000000..290c06866 --- /dev/null +++ b/apps/dashboard/src/lib/beacon/token.ts @@ -0,0 +1,30 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; +import { BEACON_INGEST_SECRET_ENV } from "@/constants/geo"; + +function getSecret(): string | null { + const secret = process.env[BEACON_INGEST_SECRET_ENV]; + return secret && secret.length > 0 ? secret : null; +} + +function isBeaconConfigured(): boolean { + return getSecret() !== null; +} + +export function deriveBeaconToken(organizationId: string): string | null { + const secret = getSecret(); + if (!secret) { + return null; + } + return createHmac("sha256", secret).update(organizationId).digest("hex"); +} + +export function verifyBeaconToken( + organizationId: string, + token: string +): boolean { + const expected = deriveBeaconToken(organizationId); + if (!expected || expected.length !== token.length) { + return false; + } + return timingSafeEqual(Buffer.from(expected), Buffer.from(token)); +} diff --git a/apps/dashboard/src/lib/geo/discover.ts b/apps/dashboard/src/lib/geo/discover.ts new file mode 100644 index 000000000..4035e179e --- /dev/null +++ b/apps/dashboard/src/lib/geo/discover.ts @@ -0,0 +1,209 @@ +import { gateway } from "@notra/ai/gateway"; +import { scrapeWebsiteForBrandAnalysis } from "@notra/ai/utils/context-dev"; +import { db } from "@notra/db/drizzle"; +import { geoPrompts, geoSettings } from "@notra/db/schema"; +import { generateText, Output } from "ai"; +import { eq } from "drizzle-orm"; +import { Effect } from "effect"; +import { + GEO_DISCOVERY_ALIAS_LIMIT, + GEO_DISCOVERY_COMPETITOR_LIMIT, + GEO_DISCOVERY_MAX_ALIASES, + GEO_DISCOVERY_MAX_BRANDED_PROMPTS, + GEO_DISCOVERY_MAX_COMPETITORS, + GEO_DISCOVERY_MAX_PROMPTS, + GEO_DISCOVERY_MAX_TOKENS, + GEO_DISCOVERY_MIN_COMPETITORS, + GEO_DISCOVERY_MIN_PROMPTS, + GEO_DISCOVERY_MODEL, + GEO_DISCOVERY_SYSTEM_PROMPT, + GEO_PROMPT_MAX_LENGTH, + GEO_PROMPT_MIN_LENGTH, +} from "@/constants/geo"; +import { GeoDiscoveryError } from "@/lib/geo/errors"; +import { geoWebsiteDiscoverySchema } from "@/schemas/geo"; +import type { + GeoGenerateFromWebsiteResult, + GeoWebsiteDiscovery, +} from "@/types/geo"; + +const MIN_PROMPT_LENGTH = GEO_PROMPT_MIN_LENGTH; +const MAX_PROMPT_LENGTH = GEO_PROMPT_MAX_LENGTH; + +function buildDiscoveryPrompt(url: string, content: string): string { + return `Website: ${url} + +Website content: +""" +${content} +""" + +Derive the brand tracking configuration for this company: + +1. companyName: the company or product name exactly as it brands itself. +2. aliases: up to ${GEO_DISCOVERY_MAX_ALIASES} alternative spellings that identify this company - product names, the bare domain, and common misspellings. Never include generic words that could refer to anything else. +3. competitors: between ${GEO_DISCOVERY_MIN_COMPETITORS} and ${GEO_DISCOVERY_MAX_COMPETITORS} real, named companies or products that compete in the same category. +4. prompts: between ${GEO_DISCOVERY_MIN_PROMPTS} and ${GEO_DISCOVERY_MAX_PROMPTS} questions a real buyer would type into an AI assistant while researching this category. At most ${GEO_DISCOVERY_MAX_BRANDED_PROMPTS} of them may contain the company name; every other question must be unbranded and framed around the category, the problem or the buying decision, so the answer reveals whether an assistant recommends this company unprompted. Each question must be between ${MIN_PROMPT_LENGTH} and ${MAX_PROMPT_LENGTH} characters.`; +} + +function normalizeKey(value: string): string { + return value.trim().toLowerCase(); +} + +function unionValues( + existing: string[], + extracted: string[], + limit: number +): string[] { + const seen = new Set(); + const merged: string[] = []; + for (const value of [...existing, ...extracted]) { + const trimmed = value.trim(); + const key = normalizeKey(trimmed); + if (!trimmed || seen.has(key) || merged.length >= limit) { + continue; + } + seen.add(key); + merged.push(trimmed); + } + return merged; +} + +const scrapeWebsite = Effect.fn("geo.discover.scrape")(function* (url: string) { + const result = yield* Effect.tryPromise({ + try: () => scrapeWebsiteForBrandAnalysis(url), + catch: (cause) => + new GeoDiscoveryError({ message: "Failed to scrape the website", cause }), + }); + + if (!result.success) { + return yield* Effect.fail(new GeoDiscoveryError({ message: result.error })); + } + + return result.content; +}); + +const extractDiscovery = Effect.fn("geo.discover.extract")(function* ( + url: string, + content: string +) { + const result = yield* Effect.tryPromise({ + try: () => + generateText({ + model: gateway(GEO_DISCOVERY_MODEL), + output: Output.object({ schema: geoWebsiteDiscoverySchema }), + prompt: buildDiscoveryPrompt(url, content), + system: GEO_DISCOVERY_SYSTEM_PROMPT, + maxOutputTokens: GEO_DISCOVERY_MAX_TOKENS, + }), + catch: (cause) => + new GeoDiscoveryError({ + message: "Failed to analyze the website", + cause, + }), + }); + + const discovery: GeoWebsiteDiscovery = result.output; + return discovery; +}); + +export const generateGeoFromWebsite = Effect.fn("geo.generateFromWebsite")( + function* (organizationId: string, url: string) { + const content = yield* scrapeWebsite(url); + const discovery = yield* extractDiscovery(url, content); + + const existing = yield* Effect.tryPromise({ + try: () => + db.query.geoSettings.findFirst({ + where: eq(geoSettings.organizationId, organizationId), + }), + catch: (cause) => + new GeoDiscoveryError({ + message: "Failed to load GEO settings", + cause, + }), + }); + + const aliases = unionValues( + existing?.aliases ?? [], + discovery.aliases, + GEO_DISCOVERY_ALIAS_LIMIT + ); + const competitors = unionValues( + existing?.competitors ?? [], + discovery.competitors, + GEO_DISCOVERY_COMPETITOR_LIMIT + ); + const companyName = existing?.companyName ?? discovery.companyName; + + yield* Effect.tryPromise({ + try: () => + db + .insert(geoSettings) + .values({ + id: crypto.randomUUID(), + organizationId, + companyName, + aliases, + competitors, + enabled: true, + }) + .onConflictDoUpdate({ + target: geoSettings.organizationId, + set: { companyName, aliases, competitors }, + }), + catch: (cause) => + new GeoDiscoveryError({ + message: "Failed to save GEO settings", + cause, + }), + }); + + const existingPrompts = yield* Effect.tryPromise({ + try: () => + db.query.geoPrompts.findMany({ + columns: { prompt: true }, + where: eq(geoPrompts.organizationId, organizationId), + }), + catch: (cause) => + new GeoDiscoveryError({ message: "Failed to load GEO prompts", cause }), + }); + + const seen = new Set( + existingPrompts.map((row) => normalizeKey(row.prompt)) + ); + const values: { id: string; organizationId: string; prompt: string }[] = []; + for (const prompt of discovery.prompts) { + const trimmed = prompt.trim(); + const key = normalizeKey(trimmed); + if ( + trimmed.length < MIN_PROMPT_LENGTH || + trimmed.length > MAX_PROMPT_LENGTH || + seen.has(key) + ) { + continue; + } + seen.add(key); + values.push({ id: crypto.randomUUID(), organizationId, prompt: trimmed }); + } + + if (values.length > 0) { + yield* Effect.tryPromise({ + try: () => db.insert(geoPrompts).values(values), + catch: (cause) => + new GeoDiscoveryError({ + message: "Failed to save GEO prompts", + cause, + }), + }); + } + + const summary: GeoGenerateFromWebsiteResult = { + companyName, + aliases, + competitors, + promptsAdded: values.length, + }; + return summary; + } +); diff --git a/apps/dashboard/src/lib/geo/engines.ts b/apps/dashboard/src/lib/geo/engines.ts new file mode 100644 index 000000000..7d927b11e --- /dev/null +++ b/apps/dashboard/src/lib/geo/engines.ts @@ -0,0 +1,111 @@ +import { anthropic, createAnthropic } from "@ai-sdk/anthropic"; +import { createOpenAI, openai } from "@ai-sdk/openai"; +import { createPerplexity } from "@ai-sdk/perplexity"; +import { gateway } from "@notra/ai/gateway"; +import { type LanguageModel, type ToolSet, tool } from "ai"; +import { z } from "zod"; +import { + GEO_ANTHROPIC_API_KEY_ENV, + GEO_GROUNDED_ENGINES, + GEO_GROUNDED_MAX_SEARCHES, + GEO_OPENAI_API_KEY_ENV, + GEO_PERPLEXITY_API_KEY_ENV, +} from "@/constants/geo"; +import type { GeoGroundedEngine } from "@/types/geo"; + +const googleSearchTool = tool({ + type: "provider", + id: "google.google_search", + args: {}, + inputSchema: z.object({}), +}); + +const requireApiKey = (name: string): string => { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is not configured`); + } + return value; +}; + +export interface GeoGroundedInvocation { + model: LanguageModel; + tools: ToolSet; +} + +export function buildGroundedInvocation( + engine: GeoGroundedEngine +): GeoGroundedInvocation { + switch (engine.provider) { + case "gateway-openai": + return { + model: gateway(engine.model), + tools: { web_search: openai.tools.webSearch({}) }, + }; + case "gateway-anthropic": + return { + model: gateway(engine.model), + tools: { + web_search: anthropic.tools.webSearch_20250305({ + maxUses: GEO_GROUNDED_MAX_SEARCHES, + }), + }, + }; + case "gateway-google": + return { + model: gateway(engine.model), + tools: { google_search: googleSearchTool }, + }; + case "direct-openai": { + const provider = createOpenAI({ + apiKey: requireApiKey(GEO_OPENAI_API_KEY_ENV), + }); + return { + model: provider.responses(engine.model), + tools: { web_search: provider.tools.webSearch({}) }, + }; + } + case "direct-anthropic": { + const provider = createAnthropic({ + apiKey: requireApiKey(GEO_ANTHROPIC_API_KEY_ENV), + }); + return { + model: provider(engine.model), + tools: { + web_search: provider.tools.webSearch_20250305({ + maxUses: GEO_GROUNDED_MAX_SEARCHES, + }), + }, + }; + } + default: { + const provider = createPerplexity({ + apiKey: requireApiKey(GEO_PERPLEXITY_API_KEY_ENV), + }); + return { model: provider(engine.model), tools: {} }; + } + } +} + +const SUPERSEDED_BY_DIRECT: Partial< + Record +> = { + "direct-openai": "gateway-openai", + "direct-anthropic": "gateway-anthropic", +}; + +export function resolveGroundedEngines(): GeoGroundedEngine[] { + const available = GEO_GROUNDED_ENGINES.filter((engine) => + engine.isAvailable() + ); + + const superseded = new Set(); + for (const engine of available) { + const replaced = SUPERSEDED_BY_DIRECT[engine.provider]; + if (replaced) { + superseded.add(replaced); + } + } + + return available.filter((engine) => !superseded.has(engine.provider)); +} diff --git a/apps/dashboard/src/lib/geo/errors.ts b/apps/dashboard/src/lib/geo/errors.ts new file mode 100644 index 000000000..a3f424a4d --- /dev/null +++ b/apps/dashboard/src/lib/geo/errors.ts @@ -0,0 +1,11 @@ +import { Data } from "effect"; + +export class GeoScanError extends Data.TaggedError("GeoScanError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +export class GeoDiscoveryError extends Data.TaggedError("GeoDiscoveryError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} diff --git a/apps/dashboard/src/lib/geo/model-usage.ts b/apps/dashboard/src/lib/geo/model-usage.ts new file mode 100644 index 000000000..d800f76d0 --- /dev/null +++ b/apps/dashboard/src/lib/geo/model-usage.ts @@ -0,0 +1,175 @@ +import { ingestModelUsageShare } from "@notra/analytics/tinybird/client"; +import type { ModelUsageShareRow } from "@notra/analytics/tinybird/datasources"; +import { Effect } from "effect"; +import { + GEO_MODEL_USAGE_API_KEY_ENV, + GEO_MODEL_USAGE_ENDPOINT, + GEO_MODEL_USAGE_FETCH_TIMEOUT_MS, + GEO_MODEL_USAGE_INGEST_LIMIT, + GEO_MODEL_USAGE_MODELS_ENDPOINT, + GEO_MODEL_USAGE_OTHER_KEY, + GEO_MODEL_USAGE_PERIOD, + GEO_MODEL_USAGE_SOURCE, +} from "@/constants/geo"; +import { GeoScanError } from "@/lib/geo/errors"; +import { + openRouterModelsResponseSchema, + openRouterRankingsResponseSchema, +} from "@/schemas/geo"; +import type { GeoModelUsageSnapshot } from "@/types/geo"; + +const VARIANT_SUFFIX = /:[a-z0-9-]+$/; +const PREVIEW_SUFFIX = /-preview$/; +const GROUNDED_SUFFIX = /(-direct)?-grounded$/; + +interface WeeklyTotal { + model: string; + tokens: number; +} + +export function normalizeModelId(value: string): string { + return value + .toLowerCase() + .replace(GROUNDED_SUFFIX, "") + .replace(VARIANT_SUFFIX, "") + .replace(PREVIEW_SUFFIX, ""); +} + +function toCapturedAt(date: string): string { + return `${date.slice(0, 10)} 00:00:00`; +} + +const fetchJson = Effect.fn("geo.modelUsage.fetchJson")(function* ( + url: string, + apiKey: string | null +) { + const payload = yield* Effect.tryPromise({ + try: async () => { + const response = await fetch(url, { + headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}, + signal: AbortSignal.timeout(GEO_MODEL_USAGE_FETCH_TIMEOUT_MS), + }); + if (!response.ok) { + throw new Error(`${url} responded with ${response.status}`); + } + return await response.json(); + }, + catch: (cause) => + new GeoScanError({ message: `Failed to fetch ${url}`, cause }), + }); + return payload; +}); + +const fetchSlugMap = Effect.fn("geo.modelUsage.fetchSlugMap")(function* () { + const payload = yield* fetchJson(GEO_MODEL_USAGE_MODELS_ENDPOINT, null); + const parsed = openRouterModelsResponseSchema.safeParse(payload); + const slugs = new Map(); + if (!parsed.success) { + return slugs; + } + for (const model of parsed.data.data) { + if (model.canonical_slug) { + slugs.set(model.canonical_slug.toLowerCase(), model.id); + } + } + return slugs; +}); + +function resolveModelId(permaslug: string, slugs: Map): string { + const base = permaslug.toLowerCase().replace(VARIANT_SUFFIX, ""); + return normalizeModelId(slugs.get(base) ?? base); +} + +function buildRows( + weeks: Map, + slugs: Map +): ModelUsageShareRow[] { + const rows: ModelUsageShareRow[] = []; + for (const [date, entries] of weeks) { + const total = entries.reduce((sum, entry) => sum + entry.tokens, 0); + if (total <= 0) { + continue; + } + + const byModel = new Map(); + for (const entry of entries) { + if (entry.model === GEO_MODEL_USAGE_OTHER_KEY) { + continue; + } + const model = resolveModelId(entry.model, slugs); + byModel.set(model, (byModel.get(model) ?? 0) + entry.tokens); + } + + const ranked = [...byModel.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, GEO_MODEL_USAGE_INGEST_LIMIT); + + ranked.forEach(([model, tokens], index) => { + rows.push({ + captured_at: toCapturedAt(date), + source: GEO_MODEL_USAGE_SOURCE, + model, + rank: index + 1, + share: tokens / total, + raw_tokens: tokens, + }); + }); + } + return rows; +} + +export const captureModelUsageShare = Effect.fn("geo.captureModelUsageShare")( + function* () { + const apiKey = process.env[GEO_MODEL_USAGE_API_KEY_ENV]; + if (!apiKey) { + const skipped: GeoModelUsageSnapshot = { status: "skipped" }; + return skipped; + } + + const payload = yield* fetchJson( + `${GEO_MODEL_USAGE_ENDPOINT}?period=${GEO_MODEL_USAGE_PERIOD}`, + apiKey + ); + const parsed = openRouterRankingsResponseSchema.safeParse(payload); + if (!parsed.success) { + return yield* Effect.fail( + new GeoScanError({ + message: "Model usage rankings response did not match the schema", + cause: parsed.error, + }) + ); + } + + const weeks = new Map(); + for (const entry of parsed.data.data) { + const tokens = Number(entry.total_tokens); + if (!Number.isFinite(tokens)) { + continue; + } + const bucket = weeks.get(entry.date) ?? []; + bucket.push({ model: entry.model_permaslug, tokens }); + weeks.set(entry.date, bucket); + } + + const slugs = yield* fetchSlugMap(); + const rows = buildRows(weeks, slugs); + + if (rows.length === 0) { + const skipped: GeoModelUsageSnapshot = { status: "skipped" }; + return skipped; + } + + yield* Effect.tryPromise({ + try: () => ingestModelUsageShare(rows), + catch: (cause) => + new GeoScanError({ message: "Failed to ingest model usage", cause }), + }); + + const captured: GeoModelUsageSnapshot = { + status: "captured", + models: rows.length, + capturedAt: parsed.data.meta.as_of, + }; + return captured; + } +); diff --git a/apps/dashboard/src/lib/geo/prompts.ts b/apps/dashboard/src/lib/geo/prompts.ts new file mode 100644 index 000000000..85ec9c8c1 --- /dev/null +++ b/apps/dashboard/src/lib/geo/prompts.ts @@ -0,0 +1,94 @@ +import { GEO_MAX_PROMPTS } from "@/constants/geo"; +import type { + GeoBrandContext, + GeoPromptDefinition, + GeoSettings, +} from "@/types/geo"; + +const SENTENCE_SPLIT_REGEX = /[.!?]/; +const LEADING_FILLER_REGEX = + /^(we are|we're|we|it is|it's|the|a|an|our)\s+(the\s+|a\s+|an\s+)?/i; +const PLATFORM_PREFIX_REGEX = + /^(platform|tool|software|solution|service|app|product)\s+(for|to|that)\s+/i; +const WHITESPACE_REGEX = /\s+/; +const TRAILING_PUNCTUATION_REGEX = /[\s,;:.-]+$/; + +const CATEGORY_MAX_WORDS = 12; +const CATEGORY_FALLBACK = "software in this space"; +const AUDIENCE_MAX_WORDS = 14; + +function condense(value: string, maxWords: number): string { + return value + .trim() + .split(WHITESPACE_REGEX) + .slice(0, maxWords) + .join(" ") + .replace(TRAILING_PUNCTUATION_REGEX, ""); +} + +function deriveCategory(companyDescription: string | null): string { + if (!companyDescription) { + return CATEGORY_FALLBACK; + } + const firstSentence = companyDescription.split(SENTENCE_SPLIT_REGEX).at(0); + if (!firstSentence) { + return CATEGORY_FALLBACK; + } + const cleaned = firstSentence + .trim() + .replace(LEADING_FILLER_REGEX, "") + .replace(PLATFORM_PREFIX_REGEX, ""); + const condensed = condense(cleaned, CATEGORY_MAX_WORDS).toLowerCase(); + return condensed.length > 0 ? condensed : CATEGORY_FALLBACK; +} + +function deriveAudience(audience: string | null): string | null { + if (!audience) { + return null; + } + const condensed = condense(audience, AUDIENCE_MAX_WORDS).toLowerCase(); + return condensed.length > 0 ? condensed : null; +} + +export function buildGeoPrompts( + settings: GeoSettings, + brand: GeoBrandContext | null +): GeoPromptDefinition[] { + const companyName = settings.companyName; + const category = deriveCategory(brand?.companyDescription ?? null); + const audience = deriveAudience(brand?.audience ?? null); + + const prompts: GeoPromptDefinition[] = [ + { id: "best-tools", text: `What are the best tools for ${category}?` }, + { + id: "alternatives", + text: `What are good alternatives to ${companyName}?`, + }, + { + id: "recommendation", + text: `Which product would you recommend for ${category}, and why?`, + }, + { + id: "comparison", + text: `How does ${companyName} compare to other options for ${category}?`, + }, + { id: "what-is", text: `What is ${companyName} and who is it for?` }, + { + id: "how-to-choose", + text: `How should I choose a product for ${category}?`, + }, + { + id: "top-list", + text: `Can you list the top 5 companies for ${category} right now?`, + }, + ]; + + if (audience) { + prompts.push({ + id: "audience-specific", + text: `Which tools for ${category} work best for ${audience}?`, + }); + } + + return prompts.slice(0, GEO_MAX_PROMPTS); +} diff --git a/apps/dashboard/src/lib/geo/scan.ts b/apps/dashboard/src/lib/geo/scan.ts new file mode 100644 index 000000000..80b74cff3 --- /dev/null +++ b/apps/dashboard/src/lib/geo/scan.ts @@ -0,0 +1,405 @@ +import { gateway } from "@notra/ai/gateway"; +import { ingestGeoMentionChecks } from "@notra/analytics/tinybird/client"; +import type { GeoMentionCheckRow } from "@notra/analytics/tinybird/datasources"; +import { toClickHouseDateTime } from "@notra/analytics/utils/datetime"; +import { db } from "@notra/db/drizzle"; +import { brandSettings, geoPrompts, geoSettings } from "@notra/db/schema"; +import { generateText, Output, stepCountIs } from "ai"; +import { and, asc, eq } from "drizzle-orm"; +import { Effect } from "effect"; +import { + GEO_ANSWER_MAX_TOKENS, + GEO_ANSWER_SYSTEM_PROMPT, + GEO_ENGINES, + GEO_EXCERPT_MAX_LENGTH, + GEO_GROUNDED_ANSWER_MAX_TOKENS, + GEO_GROUNDED_MAX_PROMPTS, + GEO_JUDGE_MAX_TOKENS, + GEO_JUDGE_MODEL, + GEO_LANGUAGE_GROUNDED_MAX_PROMPTS, + GEO_LANGUAGE_MAX_PROMPTS, + GEO_MAX_LANGUAGES, + GEO_MAX_PROMPTS, + GEO_SCAN_CONCURRENCY, + GEO_TRANSLATION_MAX_TOKENS, +} from "@/constants/geo"; +import { + buildGroundedInvocation, + resolveGroundedEngines, +} from "@/lib/geo/engines"; +import { GeoScanError } from "@/lib/geo/errors"; +import { captureModelUsageShare } from "@/lib/geo/model-usage"; +import { buildGeoPrompts } from "@/lib/geo/prompts"; +import { + geoJudgeResultSchema, + geoTranslationResultSchema, +} from "@/schemas/geo"; +import type { + GeoGroundedEngine, + GeoJudgeResult, + GeoPromptDefinition, + GeoScanResult, + GeoSettings, +} from "@/types/geo"; + +const MAX_JUDGE_COMPETITORS = 10; +const GROUNDED_MAX_STEPS = 4; + +interface GeoCheckTask { + engine: string; + grounded: GeoGroundedEngine | null; + prompt: GeoPromptDefinition; + language: string; +} + +interface GeoCheckContext { + organizationId: string; + scanId: string; + capturedAt: string; + companyName: string; + aliases: string[]; +} + +function normalizePosition(position: number | null): number | null { + if (position === null || !Number.isFinite(position)) { + return null; + } + const rounded = Math.round(position); + return rounded >= 1 ? rounded : null; +} + +function buildJudgePrompt( + context: GeoCheckContext, + promptText: string, + answer: string +): string { + const aliasList = + context.aliases.length > 0 ? context.aliases.join(", ") : "none"; + return `Company: ${context.companyName} +Known aliases (any of these counts as a mention): ${aliasList} + +A user asked an AI assistant: +""" +${promptText} +""" + +The assistant answered: +""" +${answer} +""" + +Analyze the answer and report: +- mentioned: true if the company or any alias appears in the answer. +- position: the 1-based rank of the company among the recommended brands if the answer contains an ordered or bulleted list of brands, otherwise null. +- sentiment: the sentiment expressed toward the company ("positive", "neutral" or "negative"), or null if it is not mentioned. +- competitors: up to ${MAX_JUDGE_COMPETITORS} other brand or product names mentioned in the answer, excluding the company and its aliases. +- excerpt: at most ${GEO_EXCERPT_MAX_LENGTH} characters of the answer around the mention, or the first 200 characters of the answer if the company is not mentioned. + +The answer may be written in any language or script; count mentions of the company or its aliases regardless of language.`; +} + +const askEngine = Effect.fn("geo.askEngine")(function* ( + engine: string, + promptText: string +) { + const result = yield* Effect.tryPromise({ + try: () => + generateText({ + model: gateway(engine), + prompt: promptText, + system: GEO_ANSWER_SYSTEM_PROMPT, + maxOutputTokens: GEO_ANSWER_MAX_TOKENS, + }), + catch: (cause) => + new GeoScanError({ + message: `Engine ${engine} failed to answer`, + cause, + }), + }); + return result.text; +}); + +const askGroundedEngine = Effect.fn("geo.askGroundedEngine")(function* ( + engine: GeoGroundedEngine, + promptText: string +) { + const result = yield* Effect.tryPromise({ + try: () => { + const invocation = buildGroundedInvocation(engine); + return generateText({ + model: invocation.model, + tools: invocation.tools, + stopWhen: stepCountIs(GROUNDED_MAX_STEPS), + prompt: promptText, + system: GEO_ANSWER_SYSTEM_PROMPT, + maxOutputTokens: GEO_GROUNDED_ANSWER_MAX_TOKENS, + }); + }, + catch: (cause) => + new GeoScanError({ + message: `Grounded engine ${engine.key} failed to answer`, + cause, + }), + }); + return result.text; +}); + +const judgeAnswer = Effect.fn("geo.judgeAnswer")(function* ( + context: GeoCheckContext, + promptText: string, + answer: string +) { + const result = yield* Effect.tryPromise({ + try: () => + generateText({ + model: gateway(GEO_JUDGE_MODEL), + output: Output.object({ schema: geoJudgeResultSchema }), + prompt: buildJudgePrompt(context, promptText, answer), + system: + "You analyze AI assistant answers for brand mentions. Respond only with the requested structured data.", + maxOutputTokens: GEO_JUDGE_MAX_TOKENS, + }), + catch: (cause) => + new GeoScanError({ message: "Judge model failed", cause }), + }); + const judged: GeoJudgeResult = result.output; + return judged; +}); + +const translatePrompts = Effect.fn("geo.translatePrompts")(function* ( + language: string, + prompts: GeoPromptDefinition[] +) { + const result = yield* Effect.tryPromise({ + try: () => + generateText({ + model: gateway(GEO_JUDGE_MODEL), + output: Output.object({ schema: geoTranslationResultSchema }), + prompt: `Translate each prompt into ${language}. Keep brand and product names unchanged. Return the translations in the same order.\n\n${JSON.stringify(prompts.map((prompt) => prompt.text))}`, + system: + "You translate user prompts faithfully, preserving intent and named entities. Respond only with the requested structured data.", + maxOutputTokens: GEO_TRANSLATION_MAX_TOKENS, + }), + catch: (cause) => + new GeoScanError({ + message: `Translation to ${language} failed`, + cause, + }), + }); + const translations = result.output.translations; + if (translations.length !== prompts.length) { + return yield* Effect.fail( + new GeoScanError({ + message: `Translation to ${language} returned ${translations.length} prompts, expected ${prompts.length}`, + }) + ); + } + return prompts.map((prompt, index) => ({ + id: prompt.id, + text: translations[index] ?? prompt.text, + })); +}); + +const runGeoCheck = Effect.fn("geo.runCheck")(function* ( + context: GeoCheckContext, + task: GeoCheckTask +) { + const answer = task.grounded + ? yield* askGroundedEngine(task.grounded, task.prompt.text) + : yield* askEngine(task.engine, task.prompt.text); + const judged = yield* judgeAnswer(context, task.prompt.text, answer); + + const row: GeoMentionCheckRow = { + organization_id: context.organizationId, + scan_id: context.scanId, + engine: task.engine, + prompt_id: task.prompt.id, + prompt: task.prompt.text, + captured_at: context.capturedAt, + mentioned: judged.mentioned, + position: normalizePosition(judged.position), + sentiment: judged.sentiment, + competitors: judged.competitors.slice(0, MAX_JUDGE_COMPETITORS), + excerpt: judged.excerpt.slice(0, GEO_EXCERPT_MAX_LENGTH), + language: task.language, + }; + + return row; +}); + +export const runGeoScan = Effect.fn("geo.runScan")(function* ( + organizationId: string +) { + const settingsRow = yield* Effect.tryPromise({ + try: () => + db.query.geoSettings.findFirst({ + where: eq(geoSettings.organizationId, organizationId), + }), + catch: (cause) => + new GeoScanError({ message: "Failed to load GEO settings", cause }), + }); + + if (!settingsRow?.enabled) { + const skipped: GeoScanResult = { status: "skipped" }; + return skipped; + } + + const settings: GeoSettings = { + id: settingsRow.id, + organizationId: settingsRow.organizationId, + companyName: settingsRow.companyName, + aliases: settingsRow.aliases, + competitors: settingsRow.competitors, + languages: settingsRow.languages ?? [], + enabled: settingsRow.enabled, + createdAt: settingsRow.createdAt.toISOString(), + updatedAt: settingsRow.updatedAt.toISOString(), + }; + + const brand = yield* Effect.tryPromise({ + try: () => + db.query.brandSettings.findFirst({ + columns: { companyDescription: true, audience: true }, + where: and( + eq(brandSettings.organizationId, organizationId), + eq(brandSettings.isDefault, true) + ), + }), + catch: (cause) => + new GeoScanError({ message: "Failed to load brand settings", cause }), + }); + + const customRows = yield* Effect.tryPromise({ + try: () => + db.query.geoPrompts.findMany({ + columns: { id: true, prompt: true }, + where: and( + eq(geoPrompts.organizationId, organizationId), + eq(geoPrompts.enabled, true) + ), + orderBy: [asc(geoPrompts.createdAt)], + }), + catch: (cause) => + new GeoScanError({ message: "Failed to load GEO prompts", cause }), + }); + + const autoPrompts = buildGeoPrompts( + settings, + brand + ? { + companyDescription: brand.companyDescription, + audience: brand.audience, + } + : null + ).slice(0, GEO_MAX_PROMPTS); + + const prompts: GeoPromptDefinition[] = [ + ...autoPrompts, + ...customRows.map((row) => ({ + id: `custom-${row.id}`, + text: row.prompt, + })), + ]; + + const context: GeoCheckContext = { + organizationId, + scanId: crypto.randomUUID(), + capturedAt: toClickHouseDateTime(new Date()), + companyName: settings.companyName, + aliases: settings.aliases, + }; + + const tasks: GeoCheckTask[] = []; + for (const engine of GEO_ENGINES) { + for (const prompt of prompts) { + tasks.push({ engine, grounded: null, prompt, language: "English" }); + } + } + + const groundedEngines = resolveGroundedEngines(); + const groundedPrompts = prompts.slice(0, GEO_GROUNDED_MAX_PROMPTS); + for (const grounded of groundedEngines) { + for (const prompt of groundedPrompts) { + tasks.push({ + engine: grounded.key, + grounded, + prompt, + language: "English", + }); + } + } + + const extraLanguages = settings.languages + .filter((language) => language !== "English") + .slice(0, GEO_MAX_LANGUAGES); + for (const language of extraLanguages) { + const localized = yield* translatePrompts( + language, + prompts.slice(0, GEO_LANGUAGE_MAX_PROMPTS) + ).pipe( + Effect.catch((error: GeoScanError) => { + console.error(`[GEO] skipping language ${language}:`, error); + return Effect.succeed(null); + }) + ); + if (!localized) { + continue; + } + for (const engine of GEO_ENGINES) { + for (const prompt of localized) { + tasks.push({ engine, grounded: null, prompt, language }); + } + } + const localizedGrounded = localized.slice( + 0, + GEO_LANGUAGE_GROUNDED_MAX_PROMPTS + ); + for (const grounded of groundedEngines) { + for (const prompt of localizedGrounded) { + tasks.push({ engine: grounded.key, grounded, prompt, language }); + } + } + } + + const results = yield* Effect.forEach( + tasks, + (task) => + runGeoCheck(context, task).pipe( + Effect.catch((error: GeoScanError) => { + console.error( + `[GEO] check failed for ${task.engine}/${task.prompt.id}:`, + error + ); + return Effect.succeed(null); + }) + ), + { concurrency: GEO_SCAN_CONCURRENCY } + ); + + const rows: GeoMentionCheckRow[] = []; + for (const result of results) { + if (result) { + rows.push(result); + } + } + + yield* Effect.tryPromise({ + try: () => ingestGeoMentionChecks(rows), + catch: (cause) => + new GeoScanError({ message: "Failed to ingest GEO checks", cause }), + }); + + yield* captureModelUsageShare().pipe( + Effect.catch((error: GeoScanError) => { + console.error("[GEO] model usage snapshot failed:", error); + return Effect.succeed(null); + }) + ); + + const completed: GeoScanResult = { + status: "completed", + checks: rows.length, + mentions: rows.filter((row) => row.mentioned).length, + }; + return completed; +}); diff --git a/apps/dashboard/src/lib/hooks/use-experiments.ts b/apps/dashboard/src/lib/hooks/use-experiments.ts new file mode 100644 index 000000000..b35b9c9fe --- /dev/null +++ b/apps/dashboard/src/lib/hooks/use-experiments.ts @@ -0,0 +1,89 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import type { + ExperimentMetric, + ExperimentsResponse, +} from "@/types/experiments"; +import { dashboardOrpc } from "../orpc/query"; + +export function useExperiments(organizationId: string) { + return useQuery({ + ...dashboardOrpc.experiments.list.queryOptions({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load experiments" }, + }); +} + +function useInvalidateExperiments(organizationId: string) { + const queryClient = useQueryClient(); + return () => + queryClient.invalidateQueries({ + queryKey: dashboardOrpc.experiments.list.queryKey({ + input: { organizationId }, + }), + }); +} + +export function useExperimentCreate(organizationId: string) { + const invalidate = useInvalidateExperiments(organizationId); + return useMutation({ + mutationFn: (input: { + name: string; + hypothesis?: string; + provider: string; + variantAPostId: string; + variantBPostId: string; + metric: ExperimentMetric; + }) => dashboardOrpc.experiments.create.call({ organizationId, ...input }), + onSuccess: async () => { + toast.success("Experiment started"); + await invalidate(); + }, + onError: () => toast.error("Failed to create experiment"), + }); +} + +export function useExperimentComplete(organizationId: string) { + const invalidate = useInvalidateExperiments(organizationId); + return useMutation({ + mutationFn: (experimentId: string) => + dashboardOrpc.experiments.complete.call({ organizationId, experimentId }), + onSuccess: async (result) => { + toast.success( + result.winner === "tie" + ? "Completed: too close to call" + : `Completed: variant ${result.winner?.toUpperCase() ?? "?"} wins` + ); + await invalidate(); + }, + onError: () => toast.error("Failed to complete experiment"), + }); +} + +export function useExperimentCancel(organizationId: string) { + const invalidate = useInvalidateExperiments(organizationId); + return useMutation({ + mutationFn: (experimentId: string) => + dashboardOrpc.experiments.cancel.call({ organizationId, experimentId }), + onSuccess: async () => { + await invalidate(); + }, + onError: () => toast.error("Failed to cancel experiment"), + }); +} + +export function useExperimentRemove(organizationId: string) { + const invalidate = useInvalidateExperiments(organizationId); + return useMutation({ + mutationFn: (experimentId: string) => + dashboardOrpc.experiments.remove.call({ organizationId, experimentId }), + onSuccess: async () => { + await invalidate(); + }, + onError: () => toast.error("Failed to delete experiment"), + }); +} diff --git a/apps/dashboard/src/lib/hooks/use-geo.ts b/apps/dashboard/src/lib/hooks/use-geo.ts new file mode 100644 index 000000000..f4f7f965b --- /dev/null +++ b/apps/dashboard/src/lib/hooks/use-geo.ts @@ -0,0 +1,242 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import type { + AiTrafficResponse, + BeaconSetupResponse, + GeoCompetitorShareResponse, + GeoGenerateFromWebsiteInput, + GeoLanguageShareResponse, + GeoModelUsageResponse, + GeoOverviewResponse, + GeoPromptCreateInput, + GeoPromptDeleteInput, + GeoPromptResultsResponse, + GeoPromptToggleInput, + GeoSettingsResponse, + GeoSettingsUpsertInput, + GeoTimeseriesResponse, + GeoTrackedPromptsResponse, +} from "@/types/geo"; +import { dashboardOrpc } from "../orpc/query"; + +const DEFAULT_GEO_DAYS = 30; +const DEFAULT_COMPETITOR_DAYS = 30; + +function toErrorMessage(error: unknown, fallback: string): string { + return error instanceof Error && error.message ? error.message : fallback; +} + +export function useGeoSettings(organizationId: string) { + return useQuery({ + ...dashboardOrpc.geo.settings.queryOptions({ input: { organizationId } }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load AI visibility settings" }, + }); +} + +export function useGeoSettingsUpsert(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (input: GeoSettingsUpsertInput) => + dashboardOrpc.geo.settingsUpsert.call({ ...input, organizationId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.settings.queryKey({ + input: { organizationId }, + }), + }); + toast.success("AI visibility settings saved"); + }, + onError: (error) => { + toast.error(toErrorMessage(error, "Failed to save settings")); + }, + }); +} + +export function useGeoOverview(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.overview.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_GEO_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load AI visibility overview" }, + }); +} + +export function useGeoTimeseries(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.timeseries.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_GEO_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load AI visibility trend" }, + }); +} + +export function useGeoPromptResults(organizationId: string) { + return useQuery({ + ...dashboardOrpc.geo.promptResults.queryOptions({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load prompt results" }, + }); +} + +export function useGeoCompetitorShare(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.competitorShare.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_COMPETITOR_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load competitor share" }, + }); +} + +export function useGeoLanguageShare(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.languageShare.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_GEO_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load language performance" }, + }); +} + +export function useModelUsage(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.modelUsage.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_GEO_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load model usage share" }, + }); +} + +export function useGeoPrompts(organizationId: string) { + return useQuery({ + ...dashboardOrpc.geo.promptsList.queryOptions({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load tracked prompts" }, + }); +} + +export function useGeoPromptCreate(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (input: GeoPromptCreateInput) => + dashboardOrpc.geo.promptsCreate.call({ ...input, organizationId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.promptsList.queryKey({ + input: { organizationId }, + }), + }); + toast.success("Prompt added"); + }, + onError: (error) => { + toast.error(toErrorMessage(error, "Failed to add prompt")); + }, + }); +} + +export function useGeoPromptDelete(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (input: GeoPromptDeleteInput) => + dashboardOrpc.geo.promptsDelete.call({ ...input, organizationId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.promptsList.queryKey({ + input: { organizationId }, + }), + }); + toast.success("Prompt removed"); + }, + onError: (error) => { + toast.error(toErrorMessage(error, "Failed to remove prompt")); + }, + }); +} + +export function useGeoPromptToggle(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (input: GeoPromptToggleInput) => + dashboardOrpc.geo.promptsToggle.call({ ...input, organizationId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.promptsList.queryKey({ + input: { organizationId }, + }), + }); + }, + onError: (error) => { + toast.error(toErrorMessage(error, "Failed to update prompt")); + }, + }); +} + +export function useGeoGenerateFromWebsite(organizationId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (input: GeoGenerateFromWebsiteInput) => + dashboardOrpc.geo.generateFromWebsite.call({ ...input, organizationId }), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.settings.queryKey({ + input: { organizationId }, + }), + }), + queryClient.invalidateQueries({ + queryKey: dashboardOrpc.geo.promptsList.queryKey({ + input: { organizationId }, + }), + }), + ]); + toast.success("GEO tracking generated from website"); + }, + onError: (error) => { + toast.error( + toErrorMessage(error, "Failed to generate GEO tracking from website") + ); + }, + }); +} + +export function useGeoStartScan(organizationId: string) { + return useMutation({ + mutationFn: () => dashboardOrpc.geo.startScan.call({ organizationId }), + onSuccess: () => { + toast.success("Scan started"); + }, + onError: (error) => { + toast.error(toErrorMessage(error, "Failed to start scan")); + }, + }); +} + +export function useAiTraffic(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.geo.aiTraffic.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_GEO_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load AI traffic" }, + }); +} + +export function useBeaconSetup(organizationId: string) { + return useQuery({ + ...dashboardOrpc.geo.beaconSetup.queryOptions({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load beacon setup" }, + }); +} diff --git a/apps/dashboard/src/lib/hooks/use-social-analytics.ts b/apps/dashboard/src/lib/hooks/use-social-analytics.ts new file mode 100644 index 000000000..4390d2a5b --- /dev/null +++ b/apps/dashboard/src/lib/hooks/use-social-analytics.ts @@ -0,0 +1,116 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import type { + EngagementTimeseriesResponse, + FollowerGrowthResponse, + LeaderboardResponse, + LeaderboardWindow, + NotraAdoptionResponse, + PostingPerformanceResponse, + SocialOverviewResponse, + TopPostsResponse, +} from "@/types/analytics"; +import { dashboardOrpc } from "../orpc/query"; + +const DEFAULT_TIMESERIES_DAYS = 30; +const DEFAULT_TOP_POSTS_LIMIT = 8; + +export function useSocialOverview(organizationId: string) { + return useQuery({ + ...dashboardOrpc.analytics.overview.queryOptions({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load analytics overview" }, + }); +} + +export function useEngagementTimeseries(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.analytics.engagementTimeseries.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_TIMESERIES_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load engagement data" }, + }); +} + +export function useTopPosts(organizationId: string, limit?: number) { + return useQuery({ + ...dashboardOrpc.analytics.topPosts.queryOptions({ + input: { organizationId, limit: limit ?? DEFAULT_TOP_POSTS_LIMIT }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load top posts" }, + }); +} + +export function useFollowerGrowth(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.analytics.followerGrowth.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_TIMESERIES_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load follower growth" }, + }); +} + +const DEFAULT_PERFORMANCE_DAYS = 90; + +export function usePostingPerformance(organizationId: string, days?: number) { + return useQuery({ + ...dashboardOrpc.analytics.postingPerformance.queryOptions({ + input: { organizationId, days: days ?? DEFAULT_PERFORMANCE_DAYS }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load posting performance" }, + }); +} + +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({ + input: { organizationId }, + }), + enabled: !!organizationId, + meta: { errorMessage: "Failed to load adoption data" }, + }); +} diff --git a/apps/dashboard/src/lib/orpc/router.ts b/apps/dashboard/src/lib/orpc/router.ts index 09e4821b3..949920c7f 100644 --- a/apps/dashboard/src/lib/orpc/router.ts +++ b/apps/dashboard/src/lib/orpc/router.ts @@ -1,9 +1,12 @@ +import { analyticsRouter } from "./routers/analytics"; import { apiKeysRouter } from "./routers/api-keys"; import { attachmentsRouter } from "./routers/attachments"; import { automationRouter } from "./routers/automation"; import { brandRouter } from "./routers/brand"; import { contentRouter } from "./routers/content"; +import { experimentsRouter } from "./routers/experiments"; import { feedbackRouter } from "./routers/feedback"; +import { geoRouter } from "./routers/geo"; import { githubRouter } from "./routers/github"; import { integrationsRouter } from "./routers/integrations"; import { irisRouter } from "./routers/iris"; @@ -17,12 +20,15 @@ import { uploadRouter } from "./routers/upload"; import { userRouter } from "./routers/user"; export const dashboardRouter = { + analytics: analyticsRouter, apiKeys: apiKeysRouter, attachments: attachmentsRouter, automation: automationRouter, brand: brandRouter, content: contentRouter, + experiments: experimentsRouter, feedback: feedbackRouter, + geo: geoRouter, github: githubRouter, iris: irisRouter, integrations: integrationsRouter, diff --git a/apps/dashboard/src/lib/orpc/routers/analytics.ts b/apps/dashboard/src/lib/orpc/routers/analytics.ts new file mode 100644 index 000000000..00a458297 --- /dev/null +++ b/apps/dashboard/src/lib/orpc/routers/analytics.ts @@ -0,0 +1,595 @@ +import { + ingestSocialAccountStats, + ingestSocialAccounts, + ingestSocialPostStats, + ingestSocialPosts, + isTinybirdConfigured, + queryAccountLeaderboard, + queryEngagementTimeseries, + queryFollowerGrowth, + queryNotraAdoption, + queryPostingPerformance, + 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, + FollowerGrowthResponse, + LeaderboardAccount, + LeaderboardResponse, + NotraAdoptionResponse, + PostingPerformanceResponse, + SocialOverviewAccount, + SocialOverviewResponse, + SyncableSocialAccount, + TopPostsResponse, + TrackAccountPreviewResponse, +} from "@/types/analytics"; +import { badRequest, notFound } from "../utils/errors"; + +function toNullableNumber(value: number | bigint | null): number | null { + if (value === null) { + return 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) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const accounts = await db.query.connectedSocialAccounts.findMany({ + columns: { + id: true, + provider: true, + providerAccountId: true, + username: true, + displayName: true, + profileImageUrl: true, + verified: true, + }, + where: eq(connectedSocialAccounts.organizationId, input.organizationId), + }); + + const configured = isTinybirdConfigured(); + const result = configured + ? await querySocialOverview({ + organization_id: input.organizationId, + }).catch((error) => { + console.error("[Analytics] overview query failed:", error); + return null; + }) + : null; + + const statsByAccount = new Map( + (result?.data ?? []).map((row) => [ + `${row.provider}:${row.provider_account_id}`, + row, + ]) + ); + + const overviewAccounts: SocialOverviewAccount[] = accounts.map( + (account) => { + const stats = statsByAccount.get( + `${account.provider}:${account.providerAccountId}` + ); + return { + provider: account.provider, + providerAccountId: account.providerAccountId, + accountId: account.id, + username: account.username, + displayName: account.displayName, + profileImageUrl: account.profileImageUrl, + verified: account.verified ?? false, + followersCount: toNullableNumber(stats?.followers_count ?? null), + followingCount: toNullableNumber(stats?.following_count ?? null), + postsCount: toNullableNumber(stats?.posts_count ?? null), + trackedPosts: toNullableNumber(stats?.tracked_posts ?? null), + impressions: toNullableNumber(stats?.impressions ?? null), + likes: toNullableNumber(stats?.likes ?? null), + replies: toNullableNumber(stats?.replies ?? null), + reposts: toNullableNumber(stats?.reposts ?? null), + quotes: toNullableNumber(stats?.quotes ?? null), + bookmarks: toNullableNumber(stats?.bookmarks ?? null), + statsCapturedAt: stats?.stats_captured_at ?? null, + }; + } + ); + + return { configured, accounts: overviewAccounts }; + }), + engagementTimeseries: authorizedProcedure + .input(analyticsTimeseriesInputSchema) + .handler( + async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryEngagementTimeseries({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[Analytics] query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + day: row.day, + provider: row.provider, + providerAccountId: row.provider_account_id, + posts: Number(row.posts), + impressions: toNullableNumber(row.impressions), + likes: toNullableNumber(row.likes), + replies: toNullableNumber(row.replies), + reposts: toNullableNumber(row.reposts), + })), + }; + } + ), + topPosts: authorizedProcedure + .input(analyticsTopPostsInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [result, accounts, tracked] = await Promise.all([ + queryTopPosts({ + organization_id: input.organizationId, + limit: input.limit, + }).catch((error) => { + console.error("[Analytics] top posts query failed:", error); + return null; + }), + db.query.connectedSocialAccounts.findMany({ + columns: { + provider: true, + providerAccountId: true, + username: true, + profileImageUrl: true, + }, + where: eq( + connectedSocialAccounts.organizationId, + 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, ...tracked].map((account) => [ + `${account.provider}:${account.providerAccountId}`, + account, + ]) + ); + + return { + configured: isTinybirdConfigured(), + 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 + .input(analyticsOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [organization, firstPublishedPost, adoptionResult] = + await Promise.all([ + db.query.organizations.findFirst({ + columns: { createdAt: true }, + where: eq(organizations.id, input.organizationId), + }), + db.query.posts.findFirst({ + columns: { createdAt: true }, + where: and( + eq(posts.organizationId, input.organizationId), + eq(posts.status, "published") + ), + orderBy: [asc(posts.createdAt)], + }), + queryNotraAdoption({ organization_id: input.organizationId }).catch( + (error) => { + console.error("[Analytics] adoption query failed:", error); + return null; + } + ), + ]); + + const adoptionRow = adoptionResult?.data.at(0); + const trackedFirstPost = adoptionRow?.first_notra_post_at ?? null; + + return { + configured: isTinybirdConfigured(), + organizationCreatedAt: organization?.createdAt.toISOString() ?? null, + firstNotraPostAt: + trackedFirstPost ?? + firstPublishedPost?.createdAt.toISOString() ?? + null, + notraPosts: Number(adoptionRow?.notra_posts ?? 0), + }; + }), + postingPerformance: authorizedProcedure + .input(analyticsTimeseriesInputSchema) + .handler( + async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryPostingPerformance({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[Analytics] posting performance query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + weekday: Number(row.weekday), + posts: Number(row.posts), + engagement: Number(row.engagement), + impressions: toNullableNumber(row.impressions), + avgEngagement: Number(row.avg_engagement), + })), + }; + } + ), + 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 => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryFollowerGrowth({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[Analytics] follower growth query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + day: row.day, + provider: row.provider, + providerAccountId: row.provider_account_id, + followersCount: toNullableNumber(row.followers_count), + })), + }; + }), +}; diff --git a/apps/dashboard/src/lib/orpc/routers/experiments.ts b/apps/dashboard/src/lib/orpc/routers/experiments.ts new file mode 100644 index 000000000..b294c32b6 --- /dev/null +++ b/apps/dashboard/src/lib/orpc/routers/experiments.ts @@ -0,0 +1,272 @@ +import { + isTinybirdConfigured, + queryPostMetricsLookup, +} from "@notra/analytics/tinybird/client"; +import { db } from "@notra/db/drizzle"; +import { socialExperiments } from "@notra/db/schema"; +import { and, desc, eq } from "drizzle-orm"; +import { assertOrganizationAccess } from "@/lib/auth/organization"; +import { authorizedProcedure } from "@/lib/orpc/base"; +import { + experimentActionInputSchema, + experimentCreateInputSchema, + experimentMetricSchema, + experimentsOrganizationInputSchema, +} from "@/schemas/experiments"; +import type { + ExperimentItem, + ExperimentStatus, + ExperimentsResponse, + ExperimentVariantStats, + ExperimentWinner, +} from "@/types/experiments"; +import { + computeLeader, + computeLiftPercent, + metricValue, +} from "@/utils/experiments"; +import { notFound } from "../utils/errors"; + +interface PostLookupEntry { + content: string; + url: string | null; + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; +} + +function parseStatus(value: string): ExperimentStatus { + if (value === "completed" || value === "cancelled") { + return value; + } + return "running"; +} + +function parseWinner(value: string | null): ExperimentWinner | null { + if (value === "a" || value === "b" || value === "tie") { + return value; + } + return null; +} + +function toNullableNumber(value: number | bigint | null): number | null { + if (value === null) { + return null; + } + return Number(value); +} + +function buildVariant( + postId: string, + metric: "engagement" | "impressions" | "likes", + lookup: Map +): ExperimentVariantStats { + const entry = lookup.get(postId) ?? null; + return { + postId, + content: entry?.content ?? null, + url: entry?.url ?? null, + value: metricValue(metric, entry), + impressions: entry?.impressions ?? null, + likes: entry?.likes ?? null, + replies: entry?.replies ?? null, + reposts: entry?.reposts ?? null, + }; +} + +async function loadPostLookup( + organizationId: string, + postIds: string[] +): Promise> { + if (postIds.length === 0 || !isTinybirdConfigured()) { + return new Map(); + } + const result = await queryPostMetricsLookup({ + organization_id: organizationId, + post_ids: postIds, + }).catch((error) => { + console.error("[Experiments] post lookup failed:", error); + return null; + }); + return new Map( + (result?.data ?? []).map((row) => [ + row.platform_post_id, + { + content: row.content, + url: row.url, + impressions: toNullableNumber(row.impressions), + likes: toNullableNumber(row.likes), + replies: toNullableNumber(row.replies), + reposts: toNullableNumber(row.reposts), + }, + ]) + ); +} + +async function findExperiment(organizationId: string, experimentId: string) { + const experiment = await db.query.socialExperiments.findFirst({ + where: and( + eq(socialExperiments.id, experimentId), + eq(socialExperiments.organizationId, organizationId) + ), + }); + if (!experiment) { + throw notFound("Experiment not found"); + } + return experiment; +} + +export const experimentsRouter = { + list: authorizedProcedure + .input(experimentsOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const rows = await db.query.socialExperiments.findMany({ + where: eq(socialExperiments.organizationId, input.organizationId), + orderBy: [desc(socialExperiments.createdAt)], + }); + + const postIds = [ + ...new Set( + rows.flatMap((row) => [row.variantAPostId, row.variantBPostId]) + ), + ]; + const lookup = await loadPostLookup(input.organizationId, postIds); + + const experiments: ExperimentItem[] = rows.map((row) => { + const metric = experimentMetricSchema + .catch("engagement") + .parse(row.metric); + const variantA = buildVariant(row.variantAPostId, metric, lookup); + const variantB = buildVariant(row.variantBPostId, metric, lookup); + return { + id: row.id, + name: row.name, + hypothesis: row.hypothesis, + provider: row.provider, + metric, + status: parseStatus(row.status), + winner: parseWinner(row.winner), + startedAt: row.startedAt.toISOString(), + endedAt: row.endedAt?.toISOString() ?? null, + variantA, + variantB, + liftPercent: computeLiftPercent(variantA.value, variantB.value), + leader: computeLeader(variantA.value, variantB.value), + }; + }); + + return { configured: isTinybirdConfigured(), experiments }; + }), + create: authorizedProcedure + .input(experimentCreateInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [created] = await db + .insert(socialExperiments) + .values({ + id: crypto.randomUUID(), + organizationId: input.organizationId, + name: input.name, + hypothesis: input.hypothesis ?? null, + provider: input.provider, + variantAPostId: input.variantAPostId, + variantBPostId: input.variantBPostId, + metric: input.metric, + }) + .returning({ id: socialExperiments.id }); + + return { id: created?.id ?? null }; + }), + complete: authorizedProcedure + .input(experimentActionInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const experiment = await findExperiment( + input.organizationId, + input.experimentId + ); + const metric = experimentMetricSchema + .catch("engagement") + .parse(experiment.metric); + const lookup = await loadPostLookup(input.organizationId, [ + experiment.variantAPostId, + experiment.variantBPostId, + ]); + const valueA = metricValue( + metric, + lookup.get(experiment.variantAPostId) ?? null + ); + const valueB = metricValue( + metric, + lookup.get(experiment.variantBPostId) ?? null + ); + const winner = computeLeader(valueA, valueB); + + await db + .update(socialExperiments) + .set({ + status: "completed", + winner, + endedAt: new Date(), + }) + .where(eq(socialExperiments.id, experiment.id)); + + return { winner }; + }), + cancel: authorizedProcedure + .input(experimentActionInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const experiment = await findExperiment( + input.organizationId, + input.experimentId + ); + await db + .update(socialExperiments) + .set({ status: "cancelled", endedAt: new Date() }) + .where(eq(socialExperiments.id, experiment.id)); + + return { success: true }; + }), + remove: authorizedProcedure + .input(experimentActionInputSchema) + .handler(async ({ context, input }) => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const experiment = await findExperiment( + input.organizationId, + input.experimentId + ); + await db + .delete(socialExperiments) + .where(eq(socialExperiments.id, experiment.id)); + + return { success: true }; + }), +}; diff --git a/apps/dashboard/src/lib/orpc/routers/geo.ts b/apps/dashboard/src/lib/orpc/routers/geo.ts new file mode 100644 index 000000000..8da6ac279 --- /dev/null +++ b/apps/dashboard/src/lib/orpc/routers/geo.ts @@ -0,0 +1,638 @@ +import { + isTinybirdConfigured, + queryAiTrafficLog, + queryAiTrafficOverview, + queryAiTrafficTimeseries, + queryGeoCompetitorShare, + queryGeoLanguageShare, + queryGeoOverview, + queryGeoPromptResults, + queryGeoTimeseries, + queryModelUsageLatest, +} from "@notra/analytics/tinybird/client"; +import { db } from "@notra/db/drizzle"; +import { brandSettings, geoPrompts, geoSettings } from "@notra/db/schema"; +import { and, asc, eq } from "drizzle-orm"; +import { Effect } from "effect"; +import { + AI_TRAFFIC_DEFAULT_DAYS, + AI_TRAFFIC_DEFAULT_LOG_LIMIT, + GEO_ENGINE_LABELS, + GEO_MODEL_USAGE_ATTRIBUTION, + GEO_MODEL_USAGE_DEFAULT_LIMIT, + GEO_MODEL_USAGE_SOURCE, +} from "@/constants/geo"; +import { assertOrganizationAccess } from "@/lib/auth/organization"; +import { buildBeaconIngestUrl, buildBeaconSnippet } from "@/lib/beacon/snippet"; +import { deriveBeaconToken } from "@/lib/beacon/token"; +import { generateGeoFromWebsite } from "@/lib/geo/discover"; +import type { GeoDiscoveryError } from "@/lib/geo/errors"; +import { normalizeModelId } from "@/lib/geo/model-usage"; +import { buildGeoPrompts } from "@/lib/geo/prompts"; +import { authorizedProcedure } from "@/lib/orpc/base"; +import { badRequest, notFound } from "@/lib/orpc/utils/errors"; +import { startGeoScanRun } from "@/lib/workflows/start"; +import { + aiTrafficInputSchema, + geoGenerateFromWebsiteInputSchema, + geoModelUsageInputSchema, + geoOrganizationInputSchema, + geoPromptCreateInputSchema, + geoPromptDeleteInputSchema, + geoPromptToggleInputSchema, + geoSettingsUpsertInputSchema, + geoTimeseriesInputSchema, +} from "@/schemas/geo"; +import type { + AiTrafficResponse, + BeaconSetupResponse, + GeoCompetitorShareResponse, + GeoGenerateFromWebsiteResult, + GeoLanguageShareResponse, + GeoModelUsageResponse, + GeoModelUsageRow, + GeoOverviewResponse, + GeoPromptResultsResponse, + GeoPromptRow, + GeoSettings, + GeoSettingsResponse, + GeoTimeseriesResponse, + GeoTrackedPrompt, + GeoTrackedPromptsResponse, +} from "@/types/geo"; + +interface GeoSettingsRow { + id: string; + organizationId: string; + companyName: string; + aliases: string[]; + competitors: string[]; + languages: string[] | null; + enabled: boolean; + createdAt: Date; + updatedAt: Date; +} + +function toGeoSettings(row: GeoSettingsRow): GeoSettings { + return { + id: row.id, + organizationId: row.organizationId, + companyName: row.companyName, + aliases: row.aliases, + competitors: row.competitors, + languages: row.languages ?? [], + enabled: row.enabled, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; +} + +function toTrackedPrompt(row: GeoPromptRow): GeoTrackedPrompt { + return { + id: row.id, + prompt: row.prompt, + enabled: row.enabled, + source: "custom", + createdAt: row.createdAt.toISOString(), + }; +} + +function toNullableNumber(value: number | bigint | null): number | null { + if (value === null) { + return null; + } + return Number(value); +} + +interface EngineCoverage { + mentions: number; + checks: number; +} + +function buildCoverageByModel( + rows: { engine: string; mentions: number | bigint; checks: number | bigint }[] +): Map { + const coverage = new Map(); + for (const row of rows) { + const model = normalizeModelId(row.engine); + const entry = coverage.get(model) ?? { mentions: 0, checks: 0 }; + entry.mentions += Number(row.mentions); + entry.checks += Number(row.checks); + coverage.set(model, entry); + } + return coverage; +} + +function toModelUsageRow( + model: string, + rank: number | bigint, + share: number, + rawTokens: number | bigint | null, + coverage: EngineCoverage | undefined +): GeoModelUsageRow { + const checks = coverage?.checks ?? 0; + return { + model, + label: GEO_ENGINE_LABELS[model] ?? model, + rank: Number(rank), + share, + rawTokens: rawTokens === null ? null : Number(rawTokens), + scanned: checks > 0, + mentionRate: checks > 0 ? (coverage?.mentions ?? 0) / checks : null, + checks, + }; +} +export const geoRouter = { + settings: authorizedProcedure + .input(geoOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const row = await db.query.geoSettings.findFirst({ + where: eq(geoSettings.organizationId, input.organizationId), + }); + + return { + configured: isTinybirdConfigured(), + settings: row ? toGeoSettings(row) : null, + }; + }), + settingsUpsert: authorizedProcedure + .input(geoSettingsUpsertInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [row] = await db + .insert(geoSettings) + .values({ + id: crypto.randomUUID(), + organizationId: input.organizationId, + companyName: input.companyName, + aliases: input.aliases, + competitors: input.competitors, + languages: input.languages, + enabled: input.enabled, + }) + .onConflictDoUpdate({ + target: geoSettings.organizationId, + set: { + companyName: input.companyName, + aliases: input.aliases, + competitors: input.competitors, + languages: input.languages, + enabled: input.enabled, + }, + }) + .returning(); + + return { + configured: isTinybirdConfigured(), + settings: row ? toGeoSettings(row) : null, + }; + }), + languageShare: authorizedProcedure + .input(geoTimeseriesInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryGeoLanguageShare({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[GEO] language share query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + language: row.language_name, + checks: Number(row.checks), + mentions: Number(row.mentions), + mentionRate: Number(row.mention_rate), + avgPosition: toNullableNumber(row.avg_position), + })), + }; + }), + overview: authorizedProcedure + .input(geoTimeseriesInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryGeoOverview({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[GEO] overview query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + engines: (result?.data ?? []).map((row) => ({ + engine: row.engine, + checks: Number(row.checks), + mentions: Number(row.mentions), + mentionRate: Number(row.mention_rate), + avgPosition: toNullableNumber(row.avg_position), + lastCheckedAt: row.last_checked_at, + })), + }; + }), + timeseries: authorizedProcedure + .input(geoTimeseriesInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryGeoTimeseries({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[GEO] timeseries query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + day: row.day, + engine: row.engine, + checks: Number(row.checks), + mentions: Number(row.mentions), + })), + }; + }), + promptResults: authorizedProcedure + .input(geoOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryGeoPromptResults({ + organization_id: input.organizationId, + }).catch((error) => { + console.error("[GEO] prompt results query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + results: (result?.data ?? []).map((row) => ({ + promptId: row.prompt_id, + engine: row.engine, + prompt: row.prompt, + mentioned: row.mentioned, + position: toNullableNumber(row.position), + sentiment: row.sentiment, + excerpt: row.excerpt, + lastCheckedAt: row.last_checked_at, + })), + }; + }), + competitorShare: authorizedProcedure + .input(geoTimeseriesInputSchema) + .handler( + async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const result = await queryGeoCompetitorShare({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[GEO] competitor share query failed:", error); + return null; + }); + + return { + configured: isTinybirdConfigured(), + points: (result?.data ?? []).map((row) => ({ + brand: row.brand, + mentions: Number(row.mentions), + })), + }; + } + ), + modelUsage: authorizedProcedure + .input(geoModelUsageInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [usage, overview] = await Promise.all([ + queryModelUsageLatest({ + source: GEO_MODEL_USAGE_SOURCE, + limit: input.limit ?? GEO_MODEL_USAGE_DEFAULT_LIMIT, + }).catch((error) => { + console.error("[GEO] model usage query failed:", error); + return null; + }), + queryGeoOverview({ + organization_id: input.organizationId, + days: input.days, + }).catch((error) => { + console.error("[GEO] overview query failed:", error); + return null; + }), + ]); + + const coverage = buildCoverageByModel(overview?.data ?? []); + const rows = usage?.data ?? []; + + return { + configured: isTinybirdConfigured(), + source: GEO_MODEL_USAGE_SOURCE, + attribution: GEO_MODEL_USAGE_ATTRIBUTION, + capturedAt: rows[0]?.captured_at ?? null, + models: rows.map((row) => + toModelUsageRow( + row.model, + row.rank, + Number(row.share), + row.raw_tokens, + coverage.get(row.model) + ) + ), + }; + }), + aiTraffic: authorizedProcedure + .input(aiTrafficInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const days = input.days ?? AI_TRAFFIC_DEFAULT_DAYS; + const limit = input.limit ?? AI_TRAFFIC_DEFAULT_LOG_LIMIT; + + const [overview, timeseries, log] = await Promise.all([ + queryAiTrafficOverview({ + organization_id: input.organizationId, + days, + }).catch((error) => { + console.error("[BEACON] ai traffic overview query failed:", error); + return null; + }), + queryAiTrafficTimeseries({ + organization_id: input.organizationId, + days, + }).catch((error) => { + console.error("[BEACON] ai traffic timeseries query failed:", error); + return null; + }), + queryAiTrafficLog({ + organization_id: input.organizationId, + limit, + }).catch((error) => { + console.error("[BEACON] ai traffic log query failed:", error); + return null; + }), + ]); + + return { + configured: isTinybirdConfigured(), + agents: (overview?.data ?? []).map((row) => ({ + agent: row.agent, + category: row.category, + confidence: row.confidence, + hits: Number(row.hits), + paths: Number(row.paths), + lastSeenAt: row.last_seen_at, + })), + points: (timeseries?.data ?? []).map((row) => ({ + day: row.day, + category: row.category, + hits: Number(row.hits), + })), + log: (log?.data ?? []).map((row) => ({ + capturedAt: row.captured_at, + agent: row.agent, + category: row.category, + confidence: row.confidence, + path: row.path, + method: row.method, + referer: row.referer, + })), + }; + }), + beaconSetup: authorizedProcedure + .input(geoOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const ingestUrl = buildBeaconIngestUrl(); + + return { + ingestUrl, + token: deriveBeaconToken(input.organizationId) ?? "", + snippet: buildBeaconSnippet(ingestUrl, input.organizationId), + }; + }), + promptsList: authorizedProcedure + .input(geoOrganizationInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [customRows, settingsRow] = await Promise.all([ + db.query.geoPrompts.findMany({ + where: eq(geoPrompts.organizationId, input.organizationId), + orderBy: [asc(geoPrompts.createdAt)], + }), + db.query.geoSettings.findFirst({ + where: eq(geoSettings.organizationId, input.organizationId), + }), + ]); + + const prompts: GeoTrackedPrompt[] = customRows.map(toTrackedPrompt); + + if (!settingsRow) { + return { configured: isTinybirdConfigured(), prompts }; + } + + const brand = await db.query.brandSettings.findFirst({ + columns: { companyDescription: true, audience: true }, + where: and( + eq(brandSettings.organizationId, input.organizationId), + eq(brandSettings.isDefault, true) + ), + }); + + const autoPrompts = buildGeoPrompts( + toGeoSettings(settingsRow), + brand + ? { + companyDescription: brand.companyDescription, + audience: brand.audience, + } + : null + ); + + for (const autoPrompt of autoPrompts) { + prompts.push({ + id: autoPrompt.id, + prompt: autoPrompt.text, + enabled: true, + source: "auto", + createdAt: null, + }); + } + + return { configured: isTinybirdConfigured(), prompts }; + }), + promptsCreate: authorizedProcedure + .input(geoPromptCreateInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [row] = await db + .insert(geoPrompts) + .values({ + id: crypto.randomUUID(), + organizationId: input.organizationId, + prompt: input.prompt, + }) + .returning(); + + if (!row) { + throw badRequest("Failed to create prompt"); + } + + return toTrackedPrompt(row); + }), + promptsDelete: authorizedProcedure + .input(geoPromptDeleteInputSchema) + .handler(async ({ context, input }): Promise<{ success: boolean }> => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [row] = await db + .delete(geoPrompts) + .where( + and( + eq(geoPrompts.id, input.promptId), + eq(geoPrompts.organizationId, input.organizationId) + ) + ) + .returning(); + + if (!row) { + throw notFound("Prompt not found"); + } + + return { success: true }; + }), + promptsToggle: authorizedProcedure + .input(geoPromptToggleInputSchema) + .handler(async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const [row] = await db + .update(geoPrompts) + .set({ enabled: input.enabled }) + .where( + and( + eq(geoPrompts.id, input.promptId), + eq(geoPrompts.organizationId, input.organizationId) + ) + ) + .returning(); + + if (!row) { + throw notFound("Prompt not found"); + } + + return toTrackedPrompt(row); + }), + generateFromWebsite: authorizedProcedure + .input(geoGenerateFromWebsiteInputSchema) + .handler( + async ({ context, input }): Promise => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const outcome = await Effect.runPromise( + Effect.result(generateGeoFromWebsite(input.organizationId, input.url)) + ); + + if (outcome._tag === "Failure") { + const failure: GeoDiscoveryError = outcome.failure; + console.error("[GEO] website discovery failed:", failure); + throw badRequest(failure.message); + } + + return outcome.success; + } + ), + startScan: authorizedProcedure + .input(geoOrganizationInputSchema) + .handler(async ({ context, input }): Promise<{ runId: string }> => { + await assertOrganizationAccess({ + headers: context.headers, + organizationId: input.organizationId, + user: context.user, + }); + + const row = await db.query.geoSettings.findFirst({ + columns: { id: true }, + where: eq(geoSettings.organizationId, input.organizationId), + }); + if (!row) { + throw badRequest("Configure your brand tracking settings first"); + } + + return await startGeoScanRun({ organizationId: input.organizationId }); + }), +}; diff --git a/apps/dashboard/src/lib/social-connect/publish.ts b/apps/dashboard/src/lib/social-connect/publish.ts index 6ecd4ba73..49d497d4d 100644 --- a/apps/dashboard/src/lib/social-connect/publish.ts +++ b/apps/dashboard/src/lib/social-connect/publish.ts @@ -3,6 +3,7 @@ import { connectedSocialAccounts } from "@notra/db/schema"; import { and, eq } from "drizzle-orm"; import { Effect } from "effect"; import type { SocialPostResult } from "post-for-me/resources/social-post-results"; +import { recordPublishedSocialPost } from "@/lib/analytics/record-post"; import { getSocialConnectClient, isSocialConnectConfigured, @@ -131,6 +132,20 @@ export const publishSocialPost = Effect.fn("publishSocialPost")(function* ( ? `https://x.com/${account.username}/status/${platformPostId}` : null); + if (platformPostId) { + yield* Effect.promise(() => + recordPublishedSocialPost({ + organizationId: params.organizationId, + accountId: params.accountId, + provider: account.provider, + providerAccountId: account.providerAccountId, + platformPostId, + url: postUrl, + content: params.content, + }) + ); + } + return { postId: post.id, platformPostId, diff --git a/apps/dashboard/src/lib/workflows/start.ts b/apps/dashboard/src/lib/workflows/start.ts index e8a072c86..439912706 100644 --- a/apps/dashboard/src/lib/workflows/start.ts +++ b/apps/dashboard/src/lib/workflows/start.ts @@ -7,7 +7,9 @@ import { IRIS_START_CLAIM_SCOPE, IRIS_START_CLAIM_TTL_SECONDS, } from "@/constants/iris"; +import { socialAnalyticsSyncPayloadSchema } from "@/schemas/analytics"; import { brandGuidelinesWorkflowPayloadSchema } from "@/schemas/brand-guidelines"; +import { geoScanPayloadSchema } from "@/schemas/geo"; import { eventWorkflowPayloadSchema, scheduleWorkflowPayloadSchema, @@ -24,10 +26,12 @@ import { } from "@/workflows/brand-analysis"; import { brandGuidelinesWorkflow } from "@/workflows/brand-guidelines"; import { eventContentWorkflow } from "@/workflows/event-content"; +import { geoScanWorkflow } from "@/workflows/geo-scan"; import { irisControllerRun } from "@/workflows/iris-controller"; import { onDemandContentWorkflow } from "@/workflows/on-demand-content"; import { onboardingAgentWorkflow } from "@/workflows/onboarding-agent"; import { scheduleContentWorkflow } from "@/workflows/schedule-content"; +import { socialAnalyticsSyncWorkflow } from "@/workflows/social-analytics-sync"; export async function startBrandAnalysisRun( payload: BrandAnalysisPayload @@ -106,6 +110,22 @@ export async function startEventRun(payload: { return { runId: run.runId }; } +export async function startSocialAnalyticsSyncRun(payload: { + organizationId?: string; +}): Promise<{ runId: string }> { + const parsed = socialAnalyticsSyncPayloadSchema.parse(payload); + const run = await start(socialAnalyticsSyncWorkflow, [parsed]); + return { runId: run.runId }; +} + +export async function startGeoScanRun(payload: { + organizationId: string; +}): Promise<{ runId: string }> { + const parsed = geoScanPayloadSchema.parse(payload); + const run = await start(geoScanWorkflow, [parsed]); + return { runId: run.runId }; +} + export async function startOnDemandRun( payload: unknown ): Promise<{ runId: string }> { diff --git a/apps/dashboard/src/schemas/analytics.ts b/apps/dashboard/src/schemas/analytics.ts new file mode 100644 index 000000000..9e5b6e815 --- /dev/null +++ b/apps/dashboard/src/schemas/analytics.ts @@ -0,0 +1,40 @@ +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(), +}); + +export const analyticsOrganizationInputSchema = object({ + organizationId: string().min(1), +}); + +export const analyticsTimeseriesInputSchema = object({ + organizationId: string().min(1), + days: number().int().min(1).max(365).optional(), +}); + +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/schemas/experiments.ts b/apps/dashboard/src/schemas/experiments.ts new file mode 100644 index 000000000..81c002730 --- /dev/null +++ b/apps/dashboard/src/schemas/experiments.ts @@ -0,0 +1,23 @@ +import { object, string, enum as zodEnum } from "zod"; +import { EXPERIMENT_METRICS } from "@/constants/experiments"; + +export const experimentMetricSchema = zodEnum(EXPERIMENT_METRICS); + +export const experimentsOrganizationInputSchema = object({ + organizationId: string().min(1), +}); + +export const experimentCreateInputSchema = object({ + organizationId: string().min(1), + name: string().min(1).max(120), + hypothesis: string().max(500).optional(), + provider: string().min(1), + variantAPostId: string().min(1), + variantBPostId: string().min(1), + metric: experimentMetricSchema, +}); + +export const experimentActionInputSchema = object({ + organizationId: string().min(1), + experimentId: string().min(1), +}); diff --git a/apps/dashboard/src/schemas/geo.ts b/apps/dashboard/src/schemas/geo.ts new file mode 100644 index 000000000..c06643b7e --- /dev/null +++ b/apps/dashboard/src/schemas/geo.ts @@ -0,0 +1,150 @@ +import { SUPPORTED_LANGUAGES } from "@notra/ai/constants/languages"; +import { array, boolean, enum as enumType, number, object, string } from "zod"; +import { + GEO_DISCOVERY_MAX_ALIASES, + GEO_DISCOVERY_MAX_COMPETITORS, + GEO_DISCOVERY_MAX_PROMPTS, + GEO_DISCOVERY_MIN_COMPETITORS, + GEO_DISCOVERY_MIN_PROMPTS, + GEO_MAX_LANGUAGES, + GEO_PROMPT_MAX_LENGTH, + GEO_PROMPT_MIN_LENGTH, +} from "@/constants/geo"; +import { publicWebsiteUrlSchema } from "@/schemas/url"; + +const GEO_SUPPORTED_LANGUAGE_SET = new Set(SUPPORTED_LANGUAGES); +const MAX_ALIASES = 10; +const MAX_COMPETITORS = 10; +const MAX_JUDGE_COMPETITORS = 15; +const MAX_EXCERPT_LENGTH = 300; +const MAX_DAYS = 365; +const MAX_MODEL_USAGE_LIMIT = 50; +const MAX_AI_TRAFFIC_LOG_LIMIT = 200; +const MAX_BEACON_FIELD_LENGTH = 512; +const MAX_BEACON_METHOD_LENGTH = 16; +const MIN_PROMPT_LENGTH = GEO_PROMPT_MIN_LENGTH; +const MAX_PROMPT_LENGTH = GEO_PROMPT_MAX_LENGTH; + +export const geoSettingsUpsertInputSchema = object({ + organizationId: string().min(1), + companyName: string().min(1), + aliases: array(string().min(1)).max(MAX_ALIASES), + competitors: array(string().min(1)).max(MAX_COMPETITORS), + languages: array(string().min(1)) + .max(GEO_MAX_LANGUAGES) + .refine( + (values) => + values.every((value) => GEO_SUPPORTED_LANGUAGE_SET.has(value)), + { + message: "Unsupported language", + } + ), + enabled: boolean(), +}); + +export const geoTranslationResultSchema = object({ + translations: array(string().min(1)), +}); + +export const geoOrganizationInputSchema = object({ + organizationId: string().min(1), +}); + +export const geoTimeseriesInputSchema = object({ + organizationId: string().min(1), + days: number().int().min(1).max(MAX_DAYS).optional(), +}); + +export const geoPromptCreateInputSchema = object({ + organizationId: string().min(1), + prompt: string().min(MIN_PROMPT_LENGTH).max(MAX_PROMPT_LENGTH), +}); + +export const geoPromptDeleteInputSchema = object({ + organizationId: string().min(1), + promptId: string().min(1), +}); + +export const geoPromptToggleInputSchema = object({ + organizationId: string().min(1), + promptId: string().min(1), + enabled: boolean(), +}); + +export const geoScanPayloadSchema = object({ + organizationId: string().min(1), +}); + +export const geoGenerateFromWebsiteInputSchema = object({ + organizationId: string().min(1), + url: publicWebsiteUrlSchema, +}); + +export const geoWebsiteDiscoverySchema = object({ + companyName: string().min(1), + aliases: array(string().min(1)).max(GEO_DISCOVERY_MAX_ALIASES), + competitors: array(string().min(1)) + .min(GEO_DISCOVERY_MIN_COMPETITORS) + .max(GEO_DISCOVERY_MAX_COMPETITORS), + prompts: array(string().min(MIN_PROMPT_LENGTH).max(MAX_PROMPT_LENGTH)) + .min(GEO_DISCOVERY_MIN_PROMPTS) + .max(GEO_DISCOVERY_MAX_PROMPTS), +}); + +export const geoModelUsageInputSchema = object({ + organizationId: string().min(1), + days: number().int().min(1).max(MAX_DAYS).optional(), + limit: number().int().min(1).max(MAX_MODEL_USAGE_LIMIT).optional(), +}); + +export const openRouterRankingsResponseSchema = object({ + meta: object({ + as_of: string().min(1), + start_date: string().min(1), + end_date: string().min(1), + }), + data: array( + object({ + date: string().min(1), + model_permaslug: string().min(1), + total_tokens: string().min(1), + }) + ), +}); + +export const openRouterModelsResponseSchema = object({ + data: array( + object({ + id: string().min(1), + canonical_slug: string().min(1).nullable().optional(), + }) + ), +}); + +export const geoJudgeResultSchema = object({ + mentioned: boolean(), + position: number().nullable(), + sentiment: enumType(["positive", "neutral", "negative"]).nullable(), + competitors: array(string()).max(MAX_JUDGE_COMPETITORS), + excerpt: string().max(MAX_EXCERPT_LENGTH), +}); + +export const aiTrafficInputSchema = object({ + organizationId: string().min(1), + days: number().int().min(1).max(MAX_DAYS).optional(), + limit: number().int().min(1).max(MAX_AI_TRAFFIC_LOG_LIMIT).optional(), +}); + +export const beaconEventSchema = object({ + token: string().min(1).max(MAX_BEACON_FIELD_LENGTH), + organizationId: string().min(1).max(MAX_BEACON_FIELD_LENGTH), + agent: string().min(1).max(MAX_BEACON_FIELD_LENGTH), + category: enumType(["training-crawler", "search-index", "assistant-browse"]), + confidence: enumType(["verified", "reported", "heuristic"]), + path: string().min(1).max(MAX_BEACON_FIELD_LENGTH), + host: string().min(1).max(MAX_BEACON_FIELD_LENGTH), + method: string().min(1).max(MAX_BEACON_METHOD_LENGTH), + referer: string().max(MAX_BEACON_FIELD_LENGTH).nullable(), + ua: string().max(MAX_BEACON_FIELD_LENGTH), + ts: string().min(1).max(MAX_BEACON_FIELD_LENGTH), +}); diff --git a/apps/dashboard/src/types/analytics.ts b/apps/dashboard/src/types/analytics.ts new file mode 100644 index 000000000..0ab4faf58 --- /dev/null +++ b/apps/dashboard/src/types/analytics.ts @@ -0,0 +1,304 @@ +import type { ChartConfig } from "@notra/ui/components/dither-kit/chart-context"; + +export interface SocialAnalyticsSyncPayload { + organizationId?: string; +} + +export interface SocialAnalyticsSyncResult { + status: "completed" | "skipped" | "invalid_payload"; + syncedAccounts?: number; + syncedPosts?: number; +} + +export interface SyncableSocialAccount { + id: string; + organizationId: string; + provider: string; + providerAccountId: string; + username: string; + displayName: string | null; + profileImageUrl: string | null; + verified: boolean; +} + +export interface TwitterPublicMetrics { + followers_count?: number; + following_count?: number; + tweet_count?: number; + listed_count?: number; +} + +export interface TwitterTweetPublicMetrics { + retweet_count?: number; + reply_count?: number; + like_count?: number; + quote_count?: number; + bookmark_count?: number; + impression_count?: number; +} + +export interface TwitterBatchUser { + id: string; + username: string; + name?: string; + verified?: boolean; + verified_type?: string; + profile_image_url?: string; + public_metrics?: TwitterPublicMetrics; +} + +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; + created_at?: string; + public_metrics?: TwitterTweetPublicMetrics; +} + +export interface TwitterTimelineResponse { + data?: TwitterTimelineTweet[]; + meta?: { + next_token?: string; + }; +} + +export interface RecordPublishedSocialPostInput { + organizationId: string; + accountId: string; + provider: string; + providerAccountId: string; + platformPostId: string; + url: string | null; + content: string; +} + +export interface SocialOverviewAccount { + provider: string; + providerAccountId: string; + accountId: string; + username: string; + displayName: string | null; + profileImageUrl: string | null; + verified: boolean; + followersCount: number | null; + followingCount: number | null; + postsCount: number | null; + trackedPosts: number | null; + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; + quotes: number | null; + bookmarks: number | null; + statsCapturedAt: string | null; +} + +export interface SocialOverviewResponse { + configured: boolean; + accounts: SocialOverviewAccount[]; +} + +export interface EngagementTimeseriesPoint { + day: string; + provider: string; + providerAccountId: string; + posts: number; + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; +} + +export interface EngagementTimeseriesResponse { + configured: boolean; + points: EngagementTimeseriesPoint[]; +} + +export interface TopPostItem { + provider: string; + platformPostId: string; + providerAccountId: string; + username: string | null; + profileImageUrl: string | null; + content: string; + url: string | null; + postedAt: string; + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; + bookmarks: number | null; + engagement: number; +} + +export interface TopPostsResponse { + configured: boolean; + posts: TopPostItem[]; +} + +export interface FollowerGrowthPoint { + day: string; + provider: string; + providerAccountId: string; + followersCount: number | null; +} + +export interface FollowerGrowthResponse { + configured: boolean; + points: FollowerGrowthPoint[]; +} + +export interface FollowerChartRow { + day: string; + [accountKey: string]: string | number; +} + +export interface AnalyticsStatCard { + label: string; + value: number | null; + hint?: string; +} + +export interface PostingPerformancePoint { + weekday: number; + posts: number; + engagement: number; + impressions: number | null; + avgEngagement: number; +} + +export interface PostingPerformanceResponse { + configured: boolean; + points: PostingPerformancePoint[]; +} + +export interface PostingPerformanceChartRow { + day: string; + avgEngagement: number; + posts: number; +} + +export interface AccountSeriesRow { + day: string; + rawDay: string; + [accountKey: string]: string | number; +} + +export interface NotraAdoptionResponse { + configured: boolean; + organizationCreatedAt: string | null; + firstNotraPostAt: string | null; + 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/components/nav.ts b/apps/dashboard/src/types/components/nav.ts index 754261d67..fe69c76f7 100644 --- a/apps/dashboard/src/types/components/nav.ts +++ b/apps/dashboard/src/types/components/nav.ts @@ -1,6 +1,11 @@ import type { IconSvgElement } from "@hugeicons/react"; -export type NavMainCategory = "none" | "workspace" | "automation" | "manage"; +export type NavMainCategory = + | "none" + | "workspace" + | "automation" + | "geo" + | "manage"; export interface NavItem { link: string; diff --git a/apps/dashboard/src/types/experiments.ts b/apps/dashboard/src/types/experiments.ts new file mode 100644 index 000000000..3885756bf --- /dev/null +++ b/apps/dashboard/src/types/experiments.ts @@ -0,0 +1,37 @@ +export type ExperimentMetric = "engagement" | "impressions" | "likes"; + +export type ExperimentStatus = "running" | "completed" | "cancelled"; + +export type ExperimentWinner = "a" | "b" | "tie"; + +export interface ExperimentVariantStats { + postId: string; + content: string | null; + url: string | null; + value: number | null; + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; +} + +export interface ExperimentItem { + id: string; + name: string; + hypothesis: string | null; + provider: string; + metric: ExperimentMetric; + status: ExperimentStatus; + winner: ExperimentWinner | null; + startedAt: string; + endedAt: string | null; + variantA: ExperimentVariantStats; + variantB: ExperimentVariantStats; + liftPercent: number | null; + leader: ExperimentWinner | null; +} + +export interface ExperimentsResponse { + configured: boolean; + experiments: ExperimentItem[]; +} diff --git a/apps/dashboard/src/types/geo.ts b/apps/dashboard/src/types/geo.ts new file mode 100644 index 000000000..29e6daeb7 --- /dev/null +++ b/apps/dashboard/src/types/geo.ts @@ -0,0 +1,320 @@ +export interface GeoSettings { + id: string; + organizationId: string; + companyName: string; + aliases: string[]; + competitors: string[]; + languages: string[]; + enabled: boolean; + createdAt: string; + updatedAt: string; +} + +export interface GeoSettingsResponse { + configured: boolean; + settings: GeoSettings | null; +} + +export interface GeoOverviewEngine { + engine: string; + checks: number; + mentions: number; + mentionRate: number; + avgPosition: number | null; + lastCheckedAt: string; +} + +export interface GeoOverviewResponse { + configured: boolean; + engines: GeoOverviewEngine[]; +} + +export interface GeoTimeseriesPoint { + day: string; + engine: string; + checks: number; + mentions: number; +} + +export interface GeoTimeseriesResponse { + configured: boolean; + points: GeoTimeseriesPoint[]; +} + +export interface GeoPromptResult { + promptId: string; + engine: string; + prompt: string; + mentioned: boolean; + position: number | null; + sentiment: string | null; + excerpt: string; + lastCheckedAt: string; +} + +export interface GeoPromptResultsResponse { + configured: boolean; + results: GeoPromptResult[]; +} + +export interface GeoCompetitorSharePoint { + brand: string; + mentions: number; +} + +export interface GeoCompetitorShareResponse { + configured: boolean; + points: GeoCompetitorSharePoint[]; +} + +export interface GeoSettingsUpsertInput { + organizationId: string; + companyName: string; + aliases: string[]; + competitors: string[]; + languages: string[]; + enabled: boolean; +} + +export interface GeoTrackedPrompt { + id: string; + prompt: string; + enabled: boolean; + source: "custom" | "auto"; + createdAt: string | null; +} + +export interface GeoPromptCreateInput { + prompt: string; +} + +export interface GeoPromptDeleteInput { + promptId: string; +} + +export interface GeoPromptToggleInput { + promptId: string; + enabled: boolean; +} + +export interface GeoPromptRow { + id: string; + organizationId: string; + prompt: string; + enabled: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface GeoTrackedPromptsResponse { + configured: boolean; + prompts: GeoTrackedPrompt[]; +} + +export interface GeoScanPayload { + organizationId: string; +} + +export interface GeoScanResult { + status: "completed" | "skipped" | "invalid_payload"; + checks?: number; + mentions?: number; +} + +export interface GeoPromptDefinition { + id: string; + text: string; +} + +export interface GeoBrandContext { + companyDescription: string | null; + audience: string | null; +} + +export interface MentionRateRow { + day: string; + rawDay: string; + [engine: string]: string | number; +} + +export type GeoGroundedProvider = + | "gateway-openai" + | "gateway-anthropic" + | "gateway-google" + | "direct-openai" + | "direct-anthropic" + | "direct-perplexity"; + +export interface GeoGroundedEngine { + key: string; + label: string; + model: string; + provider: GeoGroundedProvider; + envVar: string | null; + isAvailable: () => boolean; +} + +export interface GeoWebsiteDiscovery { + companyName: string; + aliases: string[]; + competitors: string[]; + prompts: string[]; +} + +export interface GeoGenerateFromWebsiteResult { + companyName: string; + aliases: string[]; + competitors: string[]; + promptsAdded: number; +} + +export interface GeoGenerateFromWebsiteInput { + url: string; +} + +export interface GeoModelUsageRow { + model: string; + label: string; + rank: number; + share: number; + rawTokens: number | null; + scanned: boolean; + mentionRate: number | null; + checks: number; +} + +export interface GeoModelUsageResponse { + configured: boolean; + source: string; + attribution: string; + capturedAt: string | null; + models: GeoModelUsageRow[]; +} + +export interface GeoModelUsageInput { + days?: number; + limit?: number; +} + +export interface GeoModelUsageSnapshot { + status: "captured" | "skipped"; + models?: number; + capturedAt?: string; +} + +export interface GeoJudgeResult { + mentioned: boolean; + position: number | null; + sentiment: "positive" | "neutral" | "negative" | null; + competitors: string[]; + excerpt: string; +} + +export type AiTrafficCategory = + | "training-crawler" + | "search-index" + | "assistant-browse"; + +export interface AiTrafficAgent { + agent: string; + category: string; + confidence: string; + hits: number; + paths: number; + lastSeenAt: string; +} + +export interface AiTrafficPoint { + day: string; + category: string; + hits: number; +} + +export interface AiTrafficLogEntry { + capturedAt: string; + agent: string; + category: string; + confidence: string; + path: string; + method: string; + referer: string | null; +} + +export interface AiTrafficResponse { + configured: boolean; + agents: AiTrafficAgent[]; + points: AiTrafficPoint[]; + log: AiTrafficLogEntry[]; +} + +export interface AiTrafficInput { + days?: number; + limit?: number; +} + +export interface BeaconSetupResponse { + ingestUrl: string; + token: string; + snippet: string; +} + +export type GeoPresenceStatus = + | "training-data" + | "retrieval-only" + | "invisible"; + +export interface GeoEngineFamily { + family: string; + web: GeoOverviewEngine | null; + raw: GeoOverviewEngine | null; +} + +export interface GeoHeroSummary { + visibilityRate: number | null; + grounded: boolean; + gapPoints: number | null; + bestEngine: GeoOverviewEngine | null; +} + +export interface GeoSummaryStatsProps { + engines: GeoOverviewEngine[]; + settings: GeoSettings; + promptCount: number; +} + +export interface GeoStatTile { + label: string; + value: string; + hint: string; +} + +export interface GeoLanguageSharePoint { + language: string; + checks: number; + mentions: number; + mentionRate: number; + avgPosition: number | null; +} + +export interface GeoLanguageShareResponse { + configured: boolean; + points: GeoLanguageSharePoint[]; +} + +export interface LanguagePerformanceCardProps { + points: GeoLanguageSharePoint[]; + configuredLanguages: string[]; +} + +export interface MentionRateCardProps { + engines: GeoOverviewEngine[]; +} + +export interface MentionTrendCardProps { + hero?: boolean; + points: GeoTimeseriesPoint[]; +} + +export interface AiTrafficLogCardProps { + log: AiTrafficLogEntry[]; +} 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/ai-traffic.ts b/apps/dashboard/src/utils/ai-traffic.ts new file mode 100644 index 000000000..d0c722087 --- /dev/null +++ b/apps/dashboard/src/utils/ai-traffic.ts @@ -0,0 +1,27 @@ +const PERCENT = 100; +const MIN_BAR_PERCENT = 2; + +export function hitBarWidth(hits: number, maxHits: number): number { + if (maxHits <= 0) { + return 0; + } + return Math.max((hits / maxHits) * PERCENT, MIN_BAR_PERCENT); +} + +const timestampFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", +}); + +export function formatAiTrafficTimestamp(value: string): string { + const normalized = value.includes("T") + ? value + : `${value.replace(" ", "T")}Z`; + const date = new Date(normalized); + if (Number.isNaN(date.getTime())) { + return value; + } + return timestampFormatter.format(date); +} diff --git a/apps/dashboard/src/utils/analytics-charts.ts b/apps/dashboard/src/utils/analytics-charts.ts new file mode 100644 index 000000000..023d55b59 --- /dev/null +++ b/apps/dashboard/src/utils/analytics-charts.ts @@ -0,0 +1,147 @@ +import type { + AccountSeriesRow, + AnalyticsHeroSummary, + EngagementTimeseriesPoint, + FollowerGrowthPoint, + PostingPerformanceChartRow, + PostingPerformancePoint, + SocialOverviewAccount, +} from "@/types/analytics"; + +const compactFormatter = new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, +}); + +const dayLabelFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", +}); + +const MS_PER_DAY = 86_400_000; +const WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +export function formatMetric(value: number | null): string { + if (value === null) { + return "N/A"; + } + return compactFormatter.format(value); +} + +export function formatDayLabel(day: string): string { + const date = new Date(`${day}T00:00:00Z`); + if (Number.isNaN(date.getTime())) { + return day; + } + return dayLabelFormatter.format(date); +} + +export function accountSeriesKey( + provider: string, + providerAccountId: string +): string { + return `${provider}:${providerAccountId}`; +} + +export function buildTimelineDays(days: number): string[] { + const today = new Date(); + const result: string[] = []; + for (let offset = days - 1; offset >= 0; offset -= 1) { + const date = new Date(today.getTime() - offset * MS_PER_DAY); + result.push(date.toISOString().slice(0, 10)); + } + return result; +} + +export function buildAccountSeriesRows( + timelineDays: string[], + accountKeys: string[], + points: EngagementTimeseriesPoint[], + metric: (point: EngagementTimeseriesPoint) => number +): AccountSeriesRow[] { + const valuesByDay = new Map>(); + for (const point of points) { + const key = accountSeriesKey(point.provider, point.providerAccountId); + const dayValues = valuesByDay.get(point.day) ?? new Map(); + dayValues.set(key, (dayValues.get(key) ?? 0) + metric(point)); + valuesByDay.set(point.day, dayValues); + } + + return timelineDays.map((day) => { + const row: AccountSeriesRow = { day: formatDayLabel(day), rawDay: day }; + const dayValues = valuesByDay.get(day); + for (const key of accountKeys) { + row[key] = dayValues?.get(key) ?? 0; + } + return row; + }); +} + +export function markerIndexForDate( + timelineDays: string[], + isoDate: string | null +): number | null { + if (!isoDate) { + return null; + } + const day = isoDate.slice(0, 10); + const first = timelineDays.at(0); + const last = timelineDays.at(-1); + if (!(first && last) || day > last) { + return null; + } + if (day < first) { + return null; + } + const index = timelineDays.indexOf(day); + return index === -1 ? null : index; +} + +export function sumMetric( + accounts: SocialOverviewAccount[], + metric: (account: SocialOverviewAccount) => number | null +): number | null { + let total: number | null = null; + for (const account of accounts) { + const value = metric(account); + if (value !== null) { + total = (total ?? 0) + value; + } + } + return total; +} + +export function buildPostingPerformanceRows( + points: PostingPerformancePoint[] +): PostingPerformanceChartRow[] { + const byWeekday = new Map(points.map((point) => [point.weekday, point])); + return WEEKDAY_LABELS.map((label, index) => { + const point = byWeekday.get(index + 1); + return { + day: label, + avgEngagement: point?.avgEngagement ?? 0, + posts: point?.posts ?? 0, + }; + }); +} + +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/experiments.ts b/apps/dashboard/src/utils/experiments.ts new file mode 100644 index 000000000..4d8c25c2d --- /dev/null +++ b/apps/dashboard/src/utils/experiments.ts @@ -0,0 +1,55 @@ +import { EXPERIMENT_WINNER_LIFT_THRESHOLD } from "@/constants/experiments"; +import type { ExperimentMetric, ExperimentWinner } from "@/types/experiments"; + +interface VariantMetricInput { + impressions: number | null; + likes: number | null; + replies: number | null; + reposts: number | null; +} + +export function metricValue( + metric: ExperimentMetric, + stats: VariantMetricInput | null +): number | null { + if (!stats) { + return null; + } + if (metric === "impressions") { + return stats.impressions; + } + if (metric === "likes") { + return stats.likes; + } + return (stats.likes ?? 0) + (stats.replies ?? 0) + (stats.reposts ?? 0); +} + +export function computeLiftPercent( + valueA: number | null, + valueB: number | null +): number | null { + if (valueA === null || valueB === null) { + return null; + } + const base = Math.min(valueA, valueB); + const top = Math.max(valueA, valueB); + if (base === 0) { + return top === 0 ? 0 : null; + } + return Math.round(((top - base) / base) * 100); +} + +export function computeLeader( + valueA: number | null, + valueB: number | null +): ExperimentWinner | null { + if (valueA === null || valueB === null) { + return null; + } + const base = Math.max(Math.min(valueA, valueB), 1); + const relativeLift = Math.abs(valueA - valueB) / base; + if (relativeLift < EXPERIMENT_WINNER_LIFT_THRESHOLD) { + return "tie"; + } + return valueA > valueB ? "a" : "b"; +} diff --git a/apps/dashboard/src/utils/geo-charts.ts b/apps/dashboard/src/utils/geo-charts.ts new file mode 100644 index 000000000..1288a59cc --- /dev/null +++ b/apps/dashboard/src/utils/geo-charts.ts @@ -0,0 +1,137 @@ +import type { + GeoEngineFamily, + GeoHeroSummary, + GeoOverviewEngine, + GeoTimeseriesPoint, + MentionRateRow, +} from "@/types/geo"; +import { formatDayLabel } from "@/utils/analytics-charts"; +import { isGroundedEngine } from "@/utils/geo-presence"; + +const PERCENT = 100; + +export function formatMentionRate(rate: number): string { + return `${Math.round(rate * PERCENT)}%`; +} + +const SHARE_DECIMALS = 10; +const MIN_SHARE_PERCENT = 0.1; + +export function formatUsageShare(share: number): string { + const percent = share * PERCENT; + if (percent > 0 && percent < MIN_SHARE_PERCENT) { + return `<${MIN_SHARE_PERCENT}%`; + } + return `${Math.round(percent * SHARE_DECIMALS) / SHARE_DECIMALS}%`; +} + +export function usageBarWidth(share: number, maxShare: number): number { + if (maxShare <= 0) { + return 0; + } + return Math.max((share / maxShare) * PERCENT, 2); +} + +export function buildMentionRateRows(points: GeoTimeseriesPoint[]): { + rows: MentionRateRow[]; + engines: string[]; +} { + const engines = [...new Set(points.map((point) => point.engine))]; + const byDay = new Map>(); + for (const point of points) { + const dayPoints = byDay.get(point.day) ?? new Map(); + dayPoints.set(point.engine, point); + byDay.set(point.day, dayPoints); + } + + const days = [...byDay.keys()].sort(); + const rows = days.map((day) => { + const row: MentionRateRow = { day: formatDayLabel(day), rawDay: day }; + const dayPoints = byDay.get(day); + for (const engine of engines) { + const point = dayPoints?.get(engine); + row[engine] = + point && point.checks > 0 + ? Math.round((point.mentions / point.checks) * PERCENT) + : 0; + } + return row; + }); + + return { rows, engines }; +} + +function poolRate(pool: GeoOverviewEngine[]): number | null { + const checks = pool.reduce((total, engine) => total + engine.checks, 0); + if (checks === 0) { + return null; + } + const mentions = pool.reduce((total, engine) => total + engine.mentions, 0); + return mentions / checks; +} + +export function buildGeoHeroSummary( + engines: GeoOverviewEngine[] +): GeoHeroSummary { + const grounded = engines.filter((engine) => isGroundedEngine(engine.engine)); + const raw = engines.filter((engine) => !isGroundedEngine(engine.engine)); + const groundedRate = poolRate(grounded); + const rawRate = poolRate(raw); + const visibilityRate = groundedRate ?? poolRate(engines); + const gapPoints = + groundedRate !== null && rawRate !== null + ? Math.round((groundedRate - rawRate) * PERCENT) + : null; + const bestEngine = + [...engines].sort((a, b) => b.mentionRate - a.mentionRate)[0] ?? null; + return { + visibilityRate, + grounded: groundedRate !== null, + gapPoints, + bestEngine, + }; +} + +export function gapInsight(gapPoints: number | null): string { + if (gapPoints === null) { + return "Run scans with and without web search to measure your grounding gap."; + } + if (gapPoints > 0) { + return `Engines mention you ${gapPoints} points more often with web search than from memory alone: your visibility lives in retrieval, not training data.`; + } + if (gapPoints < 0) { + return `Engines mention you ${Math.abs(gapPoints)} points more often from memory than with web search: you are already part of the training data.`; + } + return "Engines mention you as often from memory as with web search: your visibility is evenly grounded."; +} + +const GROUNDED_SUFFIX_PATTERN = /(-direct)?-grounded$/; + +export function engineFamilyOf(engine: string): string { + return engine.replace(GROUNDED_SUFFIX_PATTERN, ""); +} + +export function groupEngineFamilies( + engines: GeoOverviewEngine[] +): GeoEngineFamily[] { + const families = new Map(); + for (const engine of engines) { + const family = engineFamilyOf(engine.engine); + const entry = families.get(family) ?? { + family, + web: null, + raw: null, + }; + if (isGroundedEngine(engine.engine)) { + entry.web = engine; + } else { + entry.raw = engine; + } + families.set(family, entry); + } + return [...families.values()].sort( + (a, b) => + Math.max(b.web?.mentionRate ?? 0, b.raw?.mentionRate ?? 0) - + Math.max(a.web?.mentionRate ?? 0, a.raw?.mentionRate ?? 0) + ); +} diff --git a/apps/dashboard/src/utils/geo-presence.ts b/apps/dashboard/src/utils/geo-presence.ts new file mode 100644 index 000000000..3502222b1 --- /dev/null +++ b/apps/dashboard/src/utils/geo-presence.ts @@ -0,0 +1,34 @@ +import type { GeoPresenceStatus, GeoPromptResult } from "@/types/geo"; + +const GROUNDED_ENGINE_PATTERN = /(-direct)?-grounded$|^perplexity-sonar$/; + +export function isGroundedEngine(engine: string): boolean { + return GROUNDED_ENGINE_PATTERN.test(engine); +} + +export function classifyPromptPresence( + results: GeoPromptResult[] +): GeoPresenceStatus | null { + if (results.length === 0) { + return null; + } + let mentionedRaw = false; + let mentionedWeb = false; + for (const result of results) { + if (!result.mentioned) { + continue; + } + if (isGroundedEngine(result.engine)) { + mentionedWeb = true; + } else { + mentionedRaw = true; + } + } + if (mentionedRaw) { + return "training-data"; + } + if (mentionedWeb) { + return "retrieval-only"; + } + return "invisible"; +} 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/utils/ratelimit.ts b/apps/dashboard/src/utils/ratelimit.ts index b11050204..2538717d1 100644 --- a/apps/dashboard/src/utils/ratelimit.ts +++ b/apps/dashboard/src/utils/ratelimit.ts @@ -95,6 +95,12 @@ export const ratelimit = { prefix: "ratelimit:chat-relay", limiter: Ratelimit.slidingWindow(20, "1m"), }), + beaconIngest: new Ratelimit({ + redis, + analytics: true, + prefix: "ratelimit:beacon-ingest", + limiter: Ratelimit.slidingWindow(300, "1m"), + }), slackOAuth: new Ratelimit({ redis, analytics: true, diff --git a/apps/dashboard/src/workflows/geo-scan.ts b/apps/dashboard/src/workflows/geo-scan.ts new file mode 100644 index 000000000..7eb50dea4 --- /dev/null +++ b/apps/dashboard/src/workflows/geo-scan.ts @@ -0,0 +1,18 @@ +import { flattenError } from "zod"; +import { geoScanPayloadSchema } from "@/schemas/geo"; +import type { GeoScanPayload, GeoScanResult } from "@/types/geo"; +import { runGeoScanStep } from "./steps/geo-scan-steps"; + +export async function geoScanWorkflow( + payload: GeoScanPayload +): Promise { + "use workflow"; + + const parseResult = geoScanPayloadSchema.safeParse(payload); + if (!parseResult.success) { + console.error("[GEO] Invalid payload:", flattenError(parseResult.error)); + return { status: "invalid_payload" }; + } + + return await runGeoScanStep(parseResult.data.organizationId); +} diff --git a/apps/dashboard/src/workflows/social-analytics-sync.ts b/apps/dashboard/src/workflows/social-analytics-sync.ts new file mode 100644 index 000000000..6a1458603 --- /dev/null +++ b/apps/dashboard/src/workflows/social-analytics-sync.ts @@ -0,0 +1,40 @@ +import { flattenError } from "zod"; +import { socialAnalyticsSyncPayloadSchema } from "@/schemas/analytics"; +import type { + SocialAnalyticsSyncPayload, + SocialAnalyticsSyncResult, +} from "@/types/analytics"; +import { + listSyncableAccounts, + snapshotAccountDimensions, + syncTwitterAnalytics, +} from "./steps/social-analytics-steps"; + +export async function socialAnalyticsSyncWorkflow( + payload: SocialAnalyticsSyncPayload +): Promise { + "use workflow"; + + const parseResult = socialAnalyticsSyncPayloadSchema.safeParse(payload); + if (!parseResult.success) { + console.error( + "[Social Analytics] Invalid payload:", + flattenError(parseResult.error) + ); + return { status: "invalid_payload" }; + } + + const accounts = await listSyncableAccounts(parseResult.data.organizationId); + if (accounts.length === 0) { + return { status: "completed", syncedAccounts: 0, syncedPosts: 0 }; + } + + const syncedAccounts = await snapshotAccountDimensions(accounts); + const twitterResult = await syncTwitterAnalytics(accounts); + + return { + status: "completed", + syncedAccounts, + syncedPosts: twitterResult.posts, + }; +} diff --git a/apps/dashboard/src/workflows/steps/geo-scan-steps.ts b/apps/dashboard/src/workflows/steps/geo-scan-steps.ts new file mode 100644 index 000000000..c95fba38e --- /dev/null +++ b/apps/dashboard/src/workflows/steps/geo-scan-steps.ts @@ -0,0 +1,10 @@ +import { Effect } from "effect"; +import { runGeoScan } from "@/lib/geo/scan"; +import type { GeoScanResult } from "@/types/geo"; + +export async function runGeoScanStep( + organizationId: string +): Promise { + "use step"; + return await Effect.runPromise(runGeoScan(organizationId)); +} diff --git a/apps/dashboard/src/workflows/steps/social-analytics-steps.ts b/apps/dashboard/src/workflows/steps/social-analytics-steps.ts new file mode 100644 index 000000000..854498cec --- /dev/null +++ b/apps/dashboard/src/workflows/steps/social-analytics-steps.ts @@ -0,0 +1,123 @@ +import { + ingestSocialAccountStats, + ingestSocialAccounts, + ingestSocialPostStats, + ingestSocialPosts, + isTinybirdConfigured, +} from "@notra/analytics/tinybird/client"; +import { db } from "@notra/db/drizzle"; +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"; +import type { SyncableSocialAccount } from "@/types/analytics"; + +export async function listSyncableAccounts( + organizationId?: string +): Promise { + "use step"; + if (!isTinybirdConfigured()) { + return []; + } + const accounts = await db.query.connectedSocialAccounts.findMany({ + columns: { + id: true, + organizationId: true, + provider: true, + providerAccountId: true, + username: true, + displayName: true, + profileImageUrl: true, + verified: true, + }, + ...(organizationId + ? { where: eq(connectedSocialAccounts.organizationId, organizationId) } + : {}), + }); + + 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, + providerAccountId: account.providerAccountId, + username: account.username, + displayName: account.displayName, + 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( + accounts: SyncableSocialAccount[] +): Promise { + "use step"; + const capturedAt = new Date(); + const rows = accounts.map((account) => buildAccountRow(account, capturedAt)); + await ingestSocialAccounts(rows); + return rows.length; +} + +export async function syncTwitterAnalytics( + accounts: SyncableSocialAccount[] +): Promise<{ accountStats: number; posts: number }> { + "use step"; + const capturedAt = new Date(); + const twitterAccounts = accounts.filter( + (account) => account.provider === "twitter" + ); + const rows = await collectTwitterRows(twitterAccounts, capturedAt); + await ingestSocialAccountStats(rows.accountStats); + await ingestSocialPosts(rows.posts); + await ingestSocialPostStats(rows.postStats); + return { accountStats: rows.accountStats.length, posts: rows.posts.length }; +} diff --git a/apps/web/package.json b/apps/web/package.json index 53f70dfa9..32154728b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,7 @@ "@hugeicons/core-free-icons": "^4.1.1", "@hugeicons/react": "^1.1.5", "@neoconfetti/react": "^1.0.0", + "@notra/beacon": "workspace:*", "@notra/email": "workspace:*", "@notra/kiwi": "workspace:*", "@notra/ui": "workspace:*", diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 410055342..2ed958aa1 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -1,8 +1,21 @@ import { Tracker } from "@bydefault/vercel"; import { createDualmarkMiddleware } from "@dualmark/nextjs"; +import { createBeaconMiddleware } from "@notra/beacon/middleware"; import { after, type NextRequest, NextResponse } from "next/server"; import { HOMEPAGE_LINK_HEADER, SITE_URL } from "@/utils/urls"; +const beaconIngestUrl = process.env.BEACON_INGEST_URL; +const beaconToken = process.env.BEACON_ORG_TOKEN; +const beaconOrganizationId = process.env.BEACON_ORG_ID; +const beacon = + beaconIngestUrl && beaconToken && beaconOrganizationId + ? createBeaconMiddleware({ + ingestUrl: beaconIngestUrl, + token: beaconToken, + organizationId: beaconOrganizationId, + }) + : null; + const bydefaultToken = process.env.BYDEFAULT_TOKEN; const tracker = bydefaultToken ? new Tracker({ @@ -41,12 +54,31 @@ const dualmarkProxy = createDualmarkMiddleware({ }, }); +function trackAiTraffic(request: NextRequest) { + if (!beacon) { + return; + } + + const pending: Promise[] = []; + beacon(request, { + waitUntil: (promise) => { + pending.push(promise); + }, + }); + + if (pending.length > 0) { + after(Promise.all(pending)); + } +} + function appendLinkHeader(headers: Headers, value: string) { const existing = headers.get("Link"); headers.set("Link", existing ? `${existing}, ${value}` : value); } export async function proxy(request: NextRequest) { + trackAiTraffic(request); + if ( request.nextUrl.pathname === "/" && request.nextUrl.searchParams.get("mode") === "agent" diff --git a/bun.lock b/bun.lock index f6dafac8d..6346ace8f 100644 --- a/bun.lock +++ b/bun.lock @@ -138,6 +138,9 @@ "name": "dashboard", "version": "0.1.0", "dependencies": { + "@ai-sdk/anthropic": "3.0.84", + "@ai-sdk/openai": "3.0.71", + "@ai-sdk/perplexity": "3.0.35", "@ai-sdk/react": "3.0.208", "@aws-sdk/client-s3": "^3.990.0", "@aws-sdk/s3-request-presigner": "^3.990.0", @@ -159,6 +162,7 @@ "@lexical/utils": "^0.39.0", "@neoconfetti/react": "^1.0.0", "@notra/ai": "workspace:*", + "@notra/analytics": "workspace:*", "@notra/content-generation": "workspace:*", "@notra/db": "workspace:*", "@notra/email": "workspace:*", @@ -287,6 +291,7 @@ "@hugeicons/core-free-icons": "^4.1.1", "@hugeicons/react": "^1.1.5", "@neoconfetti/react": "^1.0.0", + "@notra/beacon": "workspace:*", "@notra/email": "workspace:*", "@notra/kiwi": "workspace:*", "@notra/ui": "workspace:*", @@ -381,6 +386,28 @@ "typescript": "5.9.2", }, }, + "packages/analytics": { + "name": "@notra/analytics", + "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:*", + "@types/node": "^24.0.0", + "typescript": "5.9.2", + }, + }, + "packages/beacon": { + "name": "@notra/beacon", + "version": "0.0.1", + "devDependencies": { + "@notra/typescript-config": "workspace:*", + "typescript": "5.9.2", + }, + }, "packages/content-generation": { "name": "@notra/content-generation", "version": "0.0.1", @@ -478,9 +505,12 @@ "@xyflow/react": "^12.10.0", "ai": "6.0.206", "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cmdk": "^1.1.1", "cnfast": "^0.0.6", "color": "^5.0.3", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.562.0", @@ -500,6 +530,7 @@ "shiki": "^3.21.0", "sonner": "^2.0.7", "streamdown": "^2.0.1", + "tailwind-merge": "^3.6.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.1", @@ -507,6 +538,8 @@ "devDependencies": { "@notra/typescript-config": "*", "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.8", "@types/node": "^22.15.3", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", @@ -539,7 +572,7 @@ "protobufjs@7": "7.6.5", }, "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.83", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-R0uwSr9TFuW/lnSE/k9kp/S9U8POXl1/S0IlKgxmAF65P8Jzlr5NwyJER9mlqca0gXbHvx14weob+2zWZfsgrg=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BIDaHmCHs6Sr5VUsEkTbbVlAN4GWjg97X9x/IfXyviLtzsXvffui9XIcZugkAi1Ri6FnvI5T5qDGh5YLnSuzRg=="], "@ai-sdk/devtools": ["@ai-sdk/devtools@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "4.0.1", "@hono/node-server": "^1.19.13", "hono": "^4.12.25" }, "bin": { "devtools": "bin/cli.js" } }, "sha512-Yak6fCfooo+4/DhPGlMlhte89swJtw4OKI7epX3tFs/0/CDDQ/fw2FyaoP2Xv1TQaAqy7lbaVfJbghAI5sJ3hA=="], @@ -547,7 +580,9 @@ "@ai-sdk/mcp": ["@ai-sdk/mcp@1.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-03JEdJtB+K/I7a85GWJjrB+ypYY++ZFDTp2YJ5Wd0e0raZdFpBLvvX9LJIxPF1LRbs7PMtx6TIS1YMP0Dece+w=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.108", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-apM/hmw4ZinxPK/0vfuuiZ6MwoJmyoBjwTErgTqKVOyFhJxoYP9+SD4jV6yrFIzhuTv4X6Cx5jmv62baQefAlQ=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j6eBAa5oHFZ4U5CxpIV3T4zXNM/BviodNCZCL1qHkA4aqkwK9iQ18TWYz2DZcXpw4BO5pikKzqpXORxb1EnZGA=="], + + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TAWWUF2W+qJrjtWhKN61o0dNguqS7NTDpILlsa39TTeCBZghlyLtiEIGjeclWzP/P/InicXza8ebw+dkGN36lg=="], "@ai-sdk/provider": ["@ai-sdk/provider@4.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6p3C/vGqVIjcptBu1DnVd/BZJ2wWmV9TUv9192vT6ZvT9KNED8EwRTqyqFpoQZKgSbMDSvBSq3dqR524Nt/Crw=="], @@ -1243,6 +1278,10 @@ "@notra/ai": ["@notra/ai@workspace:packages/ai"], + "@notra/analytics": ["@notra/analytics@workspace:packages/analytics"], + + "@notra/beacon": ["@notra/beacon@workspace:packages/beacon"], + "@notra/content-generation": ["@notra/content-generation@workspace:packages/content-generation"], "@notra/db": ["@notra/db@workspace:packages/db"], @@ -2033,6 +2072,8 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.1", "", {}, "sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA=="], + "@tinybirdco/sdk": ["@tinybirdco/sdk@0.0.82", "", { "dependencies": { "@clack/prompts": "^1.0.0", "chokidar": "^4.0.0", "commander": "^12.0.0", "dotenv": "^16.0.0", "esbuild": "^0.25.0", "picocolors": "^1.1.1", "zod": "^3.25.0" }, "bin": { "tinybird": "bin/tinybird.js" } }, "sha512-aG8LNE0FGJWlAuOvypi6rXwaLGLnBKB3dQMcQa4EH/4LKxsJ2qrDiQaTf9flaSrpktPhL/bNzhYgIF5/Nh9d/A=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -4837,9 +4878,7 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - - "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JFhJK5ynprll2FR3e+sHagJJIwvIagsNA0FLbLPq2Os4yLUK2/eiaCU0jXsADik73/hhvcPPLmD+Uo8eu5kFaQ=="], + "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], @@ -4847,9 +4886,9 @@ "@ai-sdk/mcp/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JFhJK5ynprll2FR3e+sHagJJIwvIagsNA0FLbLPq2Os4yLUK2/eiaCU0jXsADik73/hhvcPPLmD+Uo8eu5kFaQ=="], + "@ai-sdk/perplexity/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], @@ -5225,6 +5264,10 @@ "@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + "@supermemory/tools/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.83", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-R0uwSr9TFuW/lnSE/k9kp/S9U8POXl1/S0IlKgxmAF65P8Jzlr5NwyJER9mlqca0gXbHvx14weob+2zWZfsgrg=="], + + "@supermemory/tools/@ai-sdk/openai": ["@ai-sdk/openai@2.0.108", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-apM/hmw4ZinxPK/0vfuuiZ6MwoJmyoBjwTErgTqKVOyFhJxoYP9+SD4jV6yrFIzhuTv4X6Cx5jmv62baQefAlQ=="], + "@supermemory/tools/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@supermemory/tools/ai": ["ai@5.0.204", "", { "dependencies": { "@ai-sdk/gateway": "2.0.103", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-vr4bPUe9v5oJpv/fzcLCupSQODqSerP1oHXDq95oLJYw2vidZUtvRP4pLYD26WzQO/PlIxMHcuVKmjPC3dd6PA=="], @@ -5253,6 +5296,12 @@ "@tanstack/react-pacer/@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="], + "@tinybirdco/sdk/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@tinybirdco/sdk/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "@tinybirdco/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@toolwind/corner-shape/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], "@types/cors/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], @@ -6147,6 +6196,14 @@ "@stoplight/spectral-runtime/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + + "@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JFhJK5ynprll2FR3e+sHagJJIwvIagsNA0FLbLPq2Os4yLUK2/eiaCU0jXsADik73/hhvcPPLmD+Uo8eu5kFaQ=="], + + "@supermemory/tools/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + + "@supermemory/tools/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JFhJK5ynprll2FR3e+sHagJJIwvIagsNA0FLbLPq2Os4yLUK2/eiaCU0jXsADik73/hhvcPPLmD+Uo8eu5kFaQ=="], + "@supermemory/tools/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.103", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.27", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f3NmeCxHOmZTIk/VG3nVrF6LnuU+S/Ls4aUBSKOI1QF+xtkF6FnREsTkSvcbzoJSsO6Jr7OyQXXEWGFDAMobLQ=="], "@supermemory/tools/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], diff --git a/packages/analytics/package.json b/packages/analytics/package.json new file mode 100644 index 000000000..bac6a1819 --- /dev/null +++ b/packages/analytics/package.json @@ -0,0 +1,25 @@ +{ + "name": "@notra/analytics", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "check-types": "tsc --noEmit", + "tinybird:dev": "tinybird dev", + "tinybird:build": "tinybird build", + "tinybird:deploy": "tinybird deploy" + }, + "exports": { + "./*": "./src/*.ts" + }, + "dependencies": { + "@tinybirdco/sdk": "^0.0.82", + "@upstash/redis": "^1.35.8", + "effect": "4.0.0-beta.93" + }, + "devDependencies": { + "@notra/typescript-config": "workspace:*", + "@types/node": "^24.0.0", + "typescript": "5.9.2" + } +} 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 new file mode 100644 index 000000000..a7911cb97 --- /dev/null +++ b/packages/analytics/src/tinybird/client.ts @@ -0,0 +1,474 @@ +import { type IngestResult, type QueryResult, Tinybird } from "@tinybirdco/sdk"; +import { bumpAnalyticsVersions, cachedQuery } from "../cache/query-cache"; +import type { AnalyticsCacheScope } from "../types/cache"; +import { + type AiTrafficEventRow, + aiTrafficEvents, + type GeoMentionCheckRow, + geoMentionChecks, + type ModelUsageShareRow, + modelUsageShare, + type SocialAccountRow, + type SocialAccountStatsRow, + type SocialPostRow, + type SocialPostSourceRow, + type SocialPostStatsRow, + socialAccountStats, + socialAccounts, + socialPostSources, + socialPostStats, + socialPosts, +} from "./datasources"; +import { + type AccountLeaderboardParams, + type AccountLeaderboardRow, + type AiTrafficLogRow, + type AiTrafficOverviewRow, + type AiTrafficTimeseriesRow, + accountLeaderboard, + aiTrafficLog, + aiTrafficOverview, + aiTrafficTimeseries, + type EngagementTimeseriesParams, + type EngagementTimeseriesRow, + engagementTimeseries, + type FollowerGrowthParams, + type FollowerGrowthRow, + followerGrowth, + type GeoCompetitorShareRow, + type GeoLanguageShareRow, + type GeoOverviewRow, + type GeoPromptResultsRow, + type GeoTimeseriesRow, + geoCompetitorShare, + geoLanguageShare, + geoOverview, + geoPromptResults, + geoTimeseries, + type ModelUsageLatestParams, + type ModelUsageLatestRow, + type ModelUsageTrendParams, + type ModelUsageTrendRow, + modelUsageLatest, + modelUsageTrend, + type NotraAdoptionRow, + notraAdoption, + type PostingPerformanceParams, + type PostingPerformanceRow, + type PostMetricsLookupRow, + postingPerformance, + postMetricsLookup, + type SocialOverviewParams, + type SocialOverviewRow, + socialOverview, + type TopPostsParams, + type TopPostsRow, + topPosts, +} from "./endpoints"; + +export function isTinybirdConfigured(): boolean { + return Boolean(process.env.TINYBIRD_TOKEN); +} + +function createTinybirdClient() { + return new Tinybird({ + token: process.env.TINYBIRD_TOKEN, + baseUrl: process.env.TINYBIRD_BASE_URL ?? "https://api.tinybird.co", + devMode: false, + datasources: { + socialAccounts, + socialAccountStats, + socialPosts, + socialPostStats, + socialPostSources, + geoMentionChecks, + modelUsageShare, + aiTrafficEvents, + }, + pipes: { + socialOverview, + engagementTimeseries, + topPosts, + followerGrowth, + postingPerformance, + notraAdoption, + postMetricsLookup, + geoOverview, + geoTimeseries, + geoPromptResults, + geoCompetitorShare, + geoLanguageShare, + accountLeaderboard, + modelUsageLatest, + modelUsageTrend, + aiTrafficOverview, + aiTrafficTimeseries, + aiTrafficLog, + }, + }); +} + +let cachedClient: ReturnType | null = null; + +function getTinybirdClient() { + if (!isTinybirdConfigured()) { + return null; + } + if (!cachedClient) { + cachedClient = createTinybirdClient(); + } + return cachedClient; +} + +async function ingestRows( + rows: TRow[], + scope: AnalyticsCacheScope, + organizationIds: ReadonlyArray, + ingest: ( + client: NonNullable>, + batch: TRow[] + ) => Promise +): Promise { + const client = getTinybirdClient(); + if (!client || rows.length === 0) { + return null; + } + 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, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialAccounts.ingestBatch(batch) + ); +} + +export function ingestSocialAccountStats( + rows: SocialAccountStatsRow[] +): Promise { + 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, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialPosts.ingestBatch(batch) + ); +} + +export function ingestSocialPostStats( + rows: SocialPostStatsRow[] +): Promise { + 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, + "social", + rows.map((row) => row.organization_id), + (client, batch) => client.socialPostSources.ingestBatch(batch) + ); +} + +export function ingestGeoMentionChecks( + rows: GeoMentionCheckRow[] +): Promise { + return ingestRows( + rows, + "geo", + rows.map((row) => row.organization_id), + (client, batch) => client.geoMentionChecks.ingestBatch(batch) + ); +} + +export function ingestModelUsageShare( + rows: ModelUsageShareRow[] +): Promise { + return ingestRows(rows, "model", [null], (client, batch) => + client.modelUsageShare.ingestBatch(batch) + ); +} + +export function ingestAiTrafficEvents( + rows: AiTrafficEventRow[] +): Promise { + return ingestRows( + rows, + "traffic", + rows.map((row) => row.organization_id), + (client, batch) => client.aiTrafficEvents.ingestBatch(batch) + ); +} + +export function querySocialOverview( + params: SocialOverviewParams +): Promise | null> { + return cachedPipeQuery( + "social", + "social_overview", + params, + params.organization_id, + (client) => client.socialOverview.query(params) + ); +} + +export function queryEngagementTimeseries( + params: EngagementTimeseriesParams +): Promise | null> { + return cachedPipeQuery( + "social", + "engagement_timeseries", + params, + params.organization_id, + (client) => client.engagementTimeseries.query(params) + ); +} + +export function queryTopPosts( + params: TopPostsParams +): Promise | null> { + return cachedPipeQuery( + "social", + "top_posts", + params, + params.organization_id, + (client) => client.topPosts.query(params) + ); +} + +export function queryFollowerGrowth( + params: FollowerGrowthParams +): Promise | null> { + return cachedPipeQuery( + "social", + "follower_growth", + params, + params.organization_id, + (client) => client.followerGrowth.query(params) + ); +} + +export function queryPostingPerformance( + params: PostingPerformanceParams +): Promise | null> { + return cachedPipeQuery( + "social", + "posting_performance", + params, + params.organization_id, + (client) => client.postingPerformance.query(params) + ); +} + +export function queryNotraAdoption(params: { + organization_id: string; +}): Promise | null> { + return cachedPipeQuery( + "social", + "notra_adoption", + params, + params.organization_id, + (client) => client.notraAdoption.query(params) + ); +} + +export function queryGeoOverview(params: { + organization_id: string; + days?: number; +}): Promise | null> { + return cachedPipeQuery( + "geo", + "geo_overview", + params, + params.organization_id, + (client) => client.geoOverview.query(params) + ); +} + +export function queryGeoTimeseries(params: { + organization_id: string; + days?: number; +}): Promise | null> { + return cachedPipeQuery( + "geo", + "geo_timeseries", + params, + params.organization_id, + (client) => client.geoTimeseries.query(params) + ); +} + +export function queryGeoPromptResults(params: { + organization_id: string; +}): Promise | null> { + return cachedPipeQuery( + "geo", + "geo_prompt_results", + params, + params.organization_id, + (client) => client.geoPromptResults.query(params) + ); +} + +export function queryGeoCompetitorShare(params: { + organization_id: string; + days?: number; + limit?: number; +}): Promise | null> { + return cachedPipeQuery( + "geo", + "geo_competitor_share", + params, + params.organization_id, + (client) => client.geoCompetitorShare.query(params) + ); +} + +export function queryGeoLanguageShare(params: { + organization_id: string; + days?: number; +}): Promise | null> { + return cachedPipeQuery( + "geo", + "geo_language_share", + params, + params.organization_id, + (client) => client.geoLanguageShare.query(params) + ); +} + +export function queryAccountLeaderboard( + params: AccountLeaderboardParams +): Promise | null> { + return cachedPipeQuery( + "social", + "account_leaderboard", + params, + params.organization_id, + (client) => client.accountLeaderboard.query(params) + ); +} + +export function queryPostMetricsLookup(params: { + organization_id: string; + post_ids: string[]; +}): Promise | null> { + return cachedPipeQuery( + "social", + "post_metrics_lookup", + params, + params.organization_id, + (client) => + // The SDK serializes arrays as repeated query keys (post_ids=a&post_ids=b), + // but Tinybird's Array() template reads a single comma-separated value, so + // repeated keys silently collapse to one id. Send one pre-joined value. + client.postMetricsLookup.query({ + organization_id: params.organization_id, + post_ids: [params.post_ids.join(",")], + }) + ); +} + +export function queryModelUsageLatest( + params: ModelUsageLatestParams +): Promise | null> { + return cachedPipeQuery( + "model", + "model_usage_latest", + params, + null, + (client) => client.modelUsageLatest.query(params) + ); +} + +export function queryModelUsageTrend( + params: ModelUsageTrendParams +): Promise | null> { + return cachedPipeQuery("model", "model_usage_trend", params, null, (client) => + client.modelUsageTrend.query(params) + ); +} + +export function queryAiTrafficOverview(params: { + organization_id: string; + days?: number; +}): Promise | null> { + return cachedPipeQuery( + "traffic", + "ai_traffic_overview", + params, + params.organization_id, + (client) => client.aiTrafficOverview.query(params) + ); +} + +export function queryAiTrafficTimeseries(params: { + organization_id: string; + days?: number; +}): Promise | null> { + return cachedPipeQuery( + "traffic", + "ai_traffic_timeseries", + params, + params.organization_id, + (client) => client.aiTrafficTimeseries.query(params) + ); +} + +export function queryAiTrafficLog(params: { + organization_id: string; + limit?: number; +}): Promise | null> { + return cachedPipeQuery( + "traffic", + "ai_traffic_log", + params, + params.organization_id, + (client) => client.aiTrafficLog.query(params) + ); +} diff --git a/packages/analytics/src/tinybird/datasources.ts b/packages/analytics/src/tinybird/datasources.ts new file mode 100644 index 000000000..ae06ae839 --- /dev/null +++ b/packages/analytics/src/tinybird/datasources.ts @@ -0,0 +1,271 @@ +import { defineDatasource, engine, type InferRow, t } from "@tinybirdco/sdk"; + +export const socialAccounts = defineDatasource("social_accounts", { + description: + "Connected social account dimension snapshots for Twitter/X and LinkedIn", + schema: { + organization_id: t.string(), + account_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + username: t.string(), + display_name: t.string().nullable(), + profile_image_url: t.string().nullable(), + profile_url: t.string().nullable(), + account_type: t.string().nullable(), + verified: t.bool(), + captured_at: t.dateTime(), + }, + engine: engine.replacingMergeTree({ + sortingKey: ["organization_id", "provider", "provider_account_id"], + ver: "captured_at", + }), +}); + +export const socialAccountStats = defineDatasource("social_account_stats", { + description: + "Append-only account-level stat snapshots; metrics a platform does not expose are null", + schema: { + organization_id: t.string(), + account_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + captured_at: t.dateTime(), + followers_count: t.uint64().nullable(), + following_count: t.uint64().nullable(), + posts_count: t.uint64().nullable(), + listed_count: t.uint64().nullable(), + }, + engine: engine.mergeTree({ + sortingKey: [ + "organization_id", + "provider", + "provider_account_id", + "captured_at", + ], + partitionKey: "toYYYYMM(captured_at)", + }), +}); + +export const socialPosts = defineDatasource("social_posts", { + description: + "Published post dimension rows for Twitter/X and LinkedIn, keyed by platform post id", + schema: { + organization_id: t.string(), + account_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + platform_post_id: t.string(), + url: t.string().nullable(), + content: t.string(), + posted_at: t.dateTime(), + captured_at: t.dateTime(), + }, + engine: engine.replacingMergeTree({ + sortingKey: ["organization_id", "provider", "platform_post_id"], + ver: "captured_at", + }), +}); + +export const socialPostStats = defineDatasource("social_post_stats", { + description: + "Append-only post-level stat snapshots; metrics a platform does not expose are null", + schema: { + organization_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + platform_post_id: t.string(), + captured_at: t.dateTime(), + impressions: t.uint64().nullable(), + likes: t.uint64().nullable(), + replies: t.uint64().nullable(), + reposts: t.uint64().nullable(), + quotes: t.uint64().nullable(), + bookmarks: t.uint64().nullable(), + }, + engine: engine.mergeTree({ + sortingKey: [ + "organization_id", + "provider", + "platform_post_id", + "captured_at", + ], + partitionKey: "toYYYYMM(captured_at)", + }), +}); + +export const socialPostStatsLatest = defineDatasource( + "social_post_stats_latest", + { + description: + "Materialized latest-per-post metric states; read with argMaxMerge/maxMerge instead of scanning social_post_stats", + schema: { + organization_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + platform_post_id: t.string(), + impressions_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + likes_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + replies_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + reposts_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + quotes_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + bookmarks_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + last_captured_at_state: t.aggregateFunction("max", t.dateTime()), + }, + engine: engine.aggregatingMergeTree({ + sortingKey: [ + "organization_id", + "provider", + "platform_post_id", + "provider_account_id", + ], + }), + } +); + +export const socialAccountStatsLatest = defineDatasource( + "social_account_stats_latest", + { + description: + "Materialized latest-per-account stat states; read with argMaxMerge/maxMerge instead of scanning social_account_stats", + schema: { + organization_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + account_id_state: t.aggregateFunction("argMax", t.string(), t.dateTime()), + followers_count_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + following_count_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + posts_count_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + listed_count_state: t.aggregateFunction( + "argMax", + t.uint64().nullable(), + t.dateTime() + ), + last_captured_at_state: t.aggregateFunction("max", t.dateTime()), + }, + engine: engine.aggregatingMergeTree({ + sortingKey: ["organization_id", "provider", "provider_account_id"], + }), + } +); + +export const socialPostSources = defineDatasource("social_post_sources", { + description: + "Append-only ledger marking posts that were published through Notra", + schema: { + organization_id: t.string(), + provider: t.string().lowCardinality(), + provider_account_id: t.string(), + platform_post_id: t.string(), + source: t.string().lowCardinality(), + captured_at: t.dateTime(), + }, + engine: engine.mergeTree({ + sortingKey: ["organization_id", "provider", "platform_post_id"], + }), +}); + +export const geoMentionChecks = defineDatasource("geo_mention_checks", { + description: + "AI engine mention checks: one row per prompt x engine per scan, with extracted mention data", + schema: { + organization_id: t.string(), + scan_id: t.string(), + engine: t.string().lowCardinality(), + prompt_id: t.string().lowCardinality(), + prompt: t.string(), + captured_at: t.dateTime(), + mentioned: t.bool(), + position: t.uint64().nullable(), + sentiment: t.string().lowCardinality().nullable(), + competitors: t.array(t.string()).jsonPath("$.competitors[:]"), + excerpt: t.string(), + language: t.string().lowCardinality(), + }, + engine: engine.mergeTree({ + sortingKey: ["organization_id", "engine", "prompt_id", "captured_at"], + partitionKey: "toYYYYMM(captured_at)", + }), +}); + +export const modelUsageShare = defineDatasource("model_usage_share", { + description: + "Industry-wide AI model usage snapshots. Intentionally has no organization_id: model usage share is global market data, identical for every organization", + schema: { + captured_at: t.dateTime(), + source: t.string().lowCardinality(), + model: t.string(), + rank: t.uint64(), + share: t.float64(), + raw_tokens: t.uint64().nullable(), + }, + engine: engine.mergeTree({ + sortingKey: ["source", "captured_at", "model"], + partitionKey: "toYYYYMM(captured_at)", + }), +}); + +export const aiTrafficEvents = defineDatasource("ai_traffic_events", { + description: + "Append-only log of AI agent requests to an organization's site, one row per detected hit", + schema: { + organization_id: t.string(), + agent: t.string().lowCardinality(), + category: t.string().lowCardinality(), + confidence: t.string().lowCardinality(), + path: t.string(), + host: t.string(), + method: t.string().lowCardinality(), + referer: t.string().nullable(), + captured_at: t.dateTime(), + }, + engine: engine.mergeTree({ + sortingKey: ["organization_id", "captured_at"], + partitionKey: "toYYYYMM(captured_at)", + }), +}); + +export type SocialAccountRow = InferRow; +export type SocialAccountStatsRow = InferRow; +export type SocialPostRow = InferRow; +export type SocialPostStatsRow = InferRow; +export type SocialPostSourceRow = InferRow; +export type GeoMentionCheckRow = InferRow; +export type ModelUsageShareRow = InferRow; +export type AiTrafficEventRow = InferRow; diff --git a/packages/analytics/src/tinybird/endpoints.ts b/packages/analytics/src/tinybird/endpoints.ts new file mode 100644 index 000000000..13211e895 --- /dev/null +++ b/packages/analytics/src/tinybird/endpoints.ts @@ -0,0 +1,999 @@ +import { + defineCopyPipe, + defineEndpoint, + defineMaterializedView, + type InferOutputRow, + type InferParams, + node, + p, + t, +} from "@tinybirdco/sdk"; +import { socialAccountStatsLatest, socialPostStatsLatest } from "./datasources"; + +const POST_STATS_LATEST_SQL = ` + SELECT + organization_id, + provider, + provider_account_id, + platform_post_id, + argMaxState(impressions, captured_at) AS impressions_state, + argMaxState(likes, captured_at) AS likes_state, + argMaxState(replies, captured_at) AS replies_state, + argMaxState(reposts, captured_at) AS reposts_state, + argMaxState(quotes, captured_at) AS quotes_state, + argMaxState(bookmarks, captured_at) AS bookmarks_state, + maxState(captured_at) AS last_captured_at_state + FROM social_post_stats + GROUP BY organization_id, provider, provider_account_id, platform_post_id +`; + +const ACCOUNT_STATS_LATEST_SQL = ` + SELECT + organization_id, + provider, + provider_account_id, + argMaxState(account_id, captured_at) AS account_id_state, + argMaxState(followers_count, captured_at) AS followers_count_state, + argMaxState(following_count, captured_at) AS following_count_state, + argMaxState(posts_count, captured_at) AS posts_count_state, + argMaxState(listed_count, captured_at) AS listed_count_state, + maxState(captured_at) AS last_captured_at_state + FROM social_account_stats + GROUP BY organization_id, provider, provider_account_id +`; + +export const socialPostStatsLatestMv = defineMaterializedView( + "social_post_stats_latest_mv", + { + description: "Keeps social_post_stats_latest current on every ingest batch", + datasource: socialPostStatsLatest, + nodes: [node({ name: "post_stats_latest", sql: POST_STATS_LATEST_SQL })], + } +); + +export const socialAccountStatsLatestMv = defineMaterializedView( + "social_account_stats_latest_mv", + { + description: + "Keeps social_account_stats_latest current on every ingest batch", + datasource: socialAccountStatsLatest, + nodes: [ + node({ name: "account_stats_latest", sql: ACCOUNT_STATS_LATEST_SQL }), + ], + } +); + +export const socialPostStatsLatestBackfill = defineCopyPipe( + "social_post_stats_latest_backfill", + { + description: + "One-off backfill of social_post_stats_latest from history predating the materialized view; argMax states are idempotent so reruns are safe", + datasource: socialPostStatsLatest, + copy_mode: "append", + copy_schedule: "@on-demand", + nodes: [node({ name: "post_stats_backfill", sql: POST_STATS_LATEST_SQL })], + } +); + +export const socialAccountStatsLatestBackfill = defineCopyPipe( + "social_account_stats_latest_backfill", + { + description: + "One-off backfill of social_account_stats_latest from history predating the materialized view; argMax states are idempotent so reruns are safe", + datasource: socialAccountStatsLatest, + copy_mode: "append", + copy_schedule: "@on-demand", + nodes: [ + node({ name: "account_stats_backfill", sql: ACCOUNT_STATS_LATEST_SQL }), + ], + } +); + +export const socialOverview = defineEndpoint("social_overview", { + description: + "Latest per-account snapshot with follower counts and lifetime engagement totals", + params: { + organization_id: p.string().describe("Organization id"), + }, + nodes: [ + node({ + name: "latest_account_stats", + sql: ` + SELECT + provider, + provider_account_id, + argMaxMerge(account_id_state) AS account_id, + argMaxMerge(followers_count_state) AS followers_count, + argMaxMerge(following_count_state) AS following_count, + argMaxMerge(posts_count_state) AS posts_count, + maxMerge(last_captured_at_state) AS stats_captured_at + FROM social_account_stats_latest + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, provider_account_id + `, + }), + node({ + name: "latest_post_stats", + sql: ` + SELECT + provider, + provider_account_id, + count() AS tracked_posts, + sum(impressions) AS impressions, + sum(likes) AS likes, + sum(replies) AS replies, + sum(reposts) AS reposts, + sum(quotes) AS quotes, + sum(bookmarks) AS bookmarks + FROM ( + SELECT + provider, + provider_account_id, + platform_post_id, + argMaxMerge(impressions_state) AS impressions, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts, + argMaxMerge(quotes_state) AS quotes, + argMaxMerge(bookmarks_state) AS bookmarks + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, provider_account_id, platform_post_id + ) + GROUP BY provider, provider_account_id + `, + }), + node({ + name: "overview", + sql: ` + SELECT + accounts.provider AS provider, + accounts.provider_account_id AS provider_account_id, + argMax(accounts.account_id, accounts.captured_at) AS account_id, + argMax(accounts.username, accounts.captured_at) AS username, + argMax(accounts.display_name, accounts.captured_at) AS display_name, + argMax(accounts.profile_image_url, accounts.captured_at) AS profile_image_url, + argMax(accounts.verified, accounts.captured_at) AS verified, + any(stats.followers_count) AS followers_count, + any(stats.following_count) AS following_count, + any(stats.posts_count) AS posts_count, + any(posts.tracked_posts) AS tracked_posts, + any(posts.impressions) AS impressions, + any(posts.likes) AS likes, + any(posts.replies) AS replies, + any(posts.reposts) AS reposts, + any(posts.quotes) AS quotes, + any(posts.bookmarks) AS bookmarks, + any(stats.stats_captured_at) AS stats_captured_at + FROM social_accounts AS accounts + LEFT JOIN latest_account_stats AS stats + ON stats.provider = accounts.provider + AND stats.provider_account_id = accounts.provider_account_id + LEFT JOIN latest_post_stats AS posts + ON posts.provider = accounts.provider + AND posts.provider_account_id = accounts.provider_account_id + WHERE accounts.organization_id = {{String(organization_id)}} + GROUP BY accounts.provider, accounts.provider_account_id + ORDER BY followers_count DESC NULLS LAST + `, + }), + ], + output: { + provider: t.string(), + provider_account_id: t.string(), + account_id: t.string(), + username: t.string(), + display_name: t.string().nullable(), + profile_image_url: t.string().nullable(), + verified: t.bool(), + followers_count: t.uint64().nullable(), + following_count: t.uint64().nullable(), + posts_count: t.uint64().nullable(), + tracked_posts: t.uint64().nullable(), + impressions: t.uint64().nullable(), + likes: t.uint64().nullable(), + replies: t.uint64().nullable(), + reposts: t.uint64().nullable(), + quotes: t.uint64().nullable(), + bookmarks: t.uint64().nullable(), + stats_captured_at: t.dateTime().nullable(), + }, +}); + +export const engagementTimeseries = defineEndpoint("engagement_timeseries", { + description: + "Daily post counts and engagement per account, attributed to each post's publish date", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "post_days", + 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, 30)}}) + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "latest_post_metrics", + sql: ` + SELECT + provider, + platform_post_id, + argMaxMerge(impressions_state) AS impressions, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "daily_totals", + sql: ` + SELECT + toDate(posts.first_posted_at) AS day, + posts.provider AS provider, + posts.provider_account_id AS provider_account_id, + count() AS posts, + sum(stats.impressions) AS impressions, + sum(stats.likes) AS likes, + sum(stats.replies) AS replies, + sum(stats.reposts) AS reposts + FROM post_days AS posts + LEFT JOIN latest_post_metrics AS stats + ON stats.provider = posts.provider + AND stats.platform_post_id = posts.platform_post_id + GROUP BY day, provider, provider_account_id + ORDER BY day ASC + `, + }), + ], + output: { + day: t.date(), + provider: t.string(), + provider_account_id: t.string(), + posts: t.uint64(), + impressions: t.uint64().nullable(), + likes: t.uint64().nullable(), + replies: t.uint64().nullable(), + reposts: t.uint64().nullable(), + }, +}); + +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_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_post_metrics", + sql: ` + SELECT + provider, + platform_post_id, + argMaxMerge(impressions_state) AS impressions, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + 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: { + organization_id: p.string().describe("Organization id"), + limit: p.int32().optional(10).describe("Number of posts"), + }, + nodes: [ + node({ + name: "latest_stats", + sql: ` + SELECT + provider, + platform_post_id, + argMaxMerge(impressions_state) AS impressions, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts, + argMaxMerge(bookmarks_state) AS bookmarks + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "ranked", + sql: ` + SELECT + posts.provider AS provider, + posts.platform_post_id AS platform_post_id, + argMax(posts.content, posts.captured_at) AS content, + argMax(posts.url, posts.captured_at) AS url, + argMax(posts.provider_account_id, posts.captured_at) AS provider_account_id, + min(posts.posted_at) AS posted_at, + any(stats.impressions) AS impressions, + any(stats.likes) AS likes, + any(stats.replies) AS replies, + any(stats.reposts) AS reposts, + any(stats.bookmarks) AS bookmarks, + coalesce(any(stats.likes), 0) + + coalesce(any(stats.replies), 0) + + coalesce(any(stats.reposts), 0) AS engagement + FROM social_posts AS posts + LEFT JOIN latest_stats AS stats + ON stats.provider = posts.provider + AND stats.platform_post_id = posts.platform_post_id + WHERE posts.organization_id = {{String(organization_id)}} + GROUP BY posts.provider, posts.platform_post_id + ORDER BY engagement DESC, posted_at DESC + LIMIT {{Int32(limit, 10)}} + `, + }), + ], + output: { + provider: t.string(), + platform_post_id: t.string(), + provider_account_id: t.string(), + content: t.string(), + url: t.string().nullable(), + posted_at: t.dateTime(), + impressions: t.uint64().nullable(), + likes: t.uint64().nullable(), + replies: t.uint64().nullable(), + reposts: t.uint64().nullable(), + bookmarks: t.uint64().nullable(), + engagement: t.uint64(), + }, +}); + +export const postingPerformance = defineEndpoint("posting_performance", { + description: + "Post volume and average engagement grouped by weekday of publish", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(90).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "post_weekdays", + sql: ` + SELECT + provider, + platform_post_id, + toDayOfWeek(min(posted_at)) AS weekday + FROM social_posts + WHERE organization_id = {{String(organization_id)}} + AND posted_at >= now() - toIntervalDay({{Int32(days, 90)}}) + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "post_metrics", + sql: ` + SELECT + provider, + platform_post_id, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts, + argMaxMerge(impressions_state) AS impressions + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "weekday_totals", + sql: ` + SELECT + posts.weekday AS weekday, + count() AS posts, + sum(coalesce(stats.likes, 0) + coalesce(stats.replies, 0) + coalesce(stats.reposts, 0)) AS engagement, + sum(stats.impressions) AS impressions, + round(sum(coalesce(stats.likes, 0) + coalesce(stats.replies, 0) + coalesce(stats.reposts, 0)) / count(), 1) AS avg_engagement + FROM post_weekdays AS posts + LEFT JOIN post_metrics AS stats + ON stats.provider = posts.provider + AND stats.platform_post_id = posts.platform_post_id + GROUP BY weekday + ORDER BY weekday ASC + `, + }), + ], + output: { + weekday: t.uint64(), + posts: t.uint64(), + engagement: t.uint64(), + impressions: t.uint64().nullable(), + avg_engagement: t.float64(), + }, +}); + +export type PostingPerformanceParams = InferParams; +export type PostingPerformanceRow = InferOutputRow; + +export const followerGrowth = defineEndpoint("follower_growth", { + description: "Daily follower counts per account", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "daily_followers", + sql: ` + SELECT + toDate(captured_at) AS day, + provider, + provider_account_id, + argMax(followers_count, captured_at) AS followers_count + FROM social_account_stats + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + GROUP BY day, provider, provider_account_id + ORDER BY day ASC + `, + }), + ], + output: { + day: t.date(), + provider: t.string(), + provider_account_id: t.string(), + followers_count: t.uint64().nullable(), + }, +}); + +export type SocialOverviewParams = InferParams; +export type SocialOverviewRow = InferOutputRow; +export type EngagementTimeseriesParams = InferParams< + typeof engagementTimeseries +>; +export type EngagementTimeseriesRow = InferOutputRow< + typeof engagementTimeseries +>; +export type TopPostsParams = InferParams; +export type TopPostsRow = InferOutputRow; +export type FollowerGrowthParams = InferParams; +export type FollowerGrowthRow = InferOutputRow; + +export const notraAdoption = defineEndpoint("notra_adoption", { + description: "When the organization first published through Notra", + params: { + organization_id: p.string().describe("Organization id"), + }, + nodes: [ + node({ + name: "adoption", + sql: ` + SELECT + min(captured_at) AS first_notra_post_at, + count() AS notra_posts + FROM social_post_sources + WHERE organization_id = {{String(organization_id)}} + AND source = 'notra' + `, + }), + ], + output: { + first_notra_post_at: t.dateTime().nullable(), + notra_posts: t.uint64(), + }, +}); + +export const geoOverview = defineEndpoint("geo_overview", { + description: "AI mention rate per engine over the trailing window", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "per_engine", + sql: ` + SELECT + engine, + count() AS checks, + countIf(mentioned) AS mentions, + round(countIf(mentioned) / count(), 3) AS mention_rate, + round(avgIf(position, mentioned AND position IS NOT NULL), 1) AS avg_position, + max(captured_at) AS last_checked_at + FROM geo_mention_checks + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + AND language IN ('', 'English') + GROUP BY engine + ORDER BY mention_rate DESC + `, + }), + ], + output: { + engine: t.string(), + checks: t.uint64(), + mentions: t.uint64(), + mention_rate: t.float64(), + avg_position: t.float64().nullable(), + last_checked_at: t.dateTime(), + }, +}); + +export const geoTimeseries = defineEndpoint("geo_timeseries", { + description: "Daily AI mention rate per engine", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "daily", + sql: ` + SELECT + toDate(captured_at) AS day, + engine, + count() AS checks, + countIf(mentioned) AS mentions + FROM geo_mention_checks + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + AND language IN ('', 'English') + GROUP BY day, engine + ORDER BY day ASC + `, + }), + ], + output: { + day: t.date(), + engine: t.string(), + checks: t.uint64(), + mentions: t.uint64(), + }, +}); + +export const geoPromptResults = defineEndpoint("geo_prompt_results", { + description: "Latest result per prompt and engine", + params: { + organization_id: p.string().describe("Organization id"), + }, + nodes: [ + node({ + name: "latest", + sql: ` + SELECT + prompt_id, + engine, + argMax(prompt, captured_at) AS prompt, + argMax(mentioned, captured_at) AS mentioned, + argMax(position, captured_at) AS position, + argMax(sentiment, captured_at) AS sentiment, + argMax(excerpt, captured_at) AS excerpt, + max(captured_at) AS last_checked_at + FROM geo_mention_checks + WHERE organization_id = {{String(organization_id)}} + AND language IN ('', 'English') + GROUP BY prompt_id, engine + ORDER BY prompt_id ASC, engine ASC + `, + }), + ], + output: { + prompt_id: t.string(), + engine: t.string(), + prompt: t.string(), + mentioned: t.bool(), + position: t.uint64().nullable(), + sentiment: t.string().nullable(), + excerpt: t.string(), + last_checked_at: t.dateTime(), + }, +}); + +export const geoCompetitorShare = defineEndpoint("geo_competitor_share", { + description: "Competitor brands surfaced by AI engines, by mention count", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + limit: p.int32().optional(10).describe("Max competitors"), + }, + nodes: [ + node({ + name: "share", + sql: ` + SELECT + arrayJoin(competitors) AS brand, + count() AS mentions + FROM geo_mention_checks + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + GROUP BY brand + ORDER BY mentions DESC + LIMIT {{Int32(limit, 10)}} + `, + }), + ], + output: { + brand: t.string(), + mentions: t.uint64(), + }, +}); + +export const geoLanguageShare = defineEndpoint("geo_language_share", { + description: + "Mention rate per answer language over the trailing window; legacy rows without a language count as English", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "per_language", + sql: ` + SELECT + if(language = '', 'English', language) AS language_name, + count() AS checks, + countIf(mentioned) AS mentions, + round(countIf(mentioned) / count(), 3) AS mention_rate, + round(avgIf(position, mentioned AND position IS NOT NULL), 1) AS avg_position, + max(captured_at) AS last_checked_at + FROM geo_mention_checks + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + GROUP BY language_name + ORDER BY mention_rate DESC + `, + }), + ], + output: { + language_name: t.string(), + checks: t.uint64(), + mentions: t.uint64(), + mention_rate: t.float64(), + avg_position: t.float64().nullable(), + last_checked_at: t.dateTime(), + }, +}); + +export const modelUsageLatest = defineEndpoint("model_usage_latest", { + description: + "Most recent industry-wide usage share snapshot per model, ranked by share", + params: { + source: p.string().optional("openrouter").describe("Snapshot source"), + limit: p.int32().optional(15).describe("Max models"), + }, + nodes: [ + node({ + name: "latest_capture", + sql: ` + SELECT max(captured_at) AS latest_captured_at + FROM model_usage_share + WHERE source = {{String(source, 'openrouter')}} + `, + }), + node({ + name: "latest_models", + sql: ` + SELECT + model, + any(captured_at) AS captured_value, + any(rank) AS rank_value, + any(share) AS share_value, + any(raw_tokens) AS tokens_value + FROM model_usage_share + WHERE source = {{String(source, 'openrouter')}} + AND captured_at = (SELECT latest_captured_at FROM latest_capture) + GROUP BY model + `, + }), + node({ + name: "ranked_models", + sql: ` + SELECT + model, + captured_value AS captured_at, + rank_value AS rank, + share_value AS share, + tokens_value AS raw_tokens + FROM latest_models + ORDER BY share DESC, model ASC + LIMIT {{Int32(limit, 15)}} + `, + }), + ], + output: { + model: t.string(), + captured_at: t.dateTime(), + rank: t.uint64(), + share: t.float64(), + raw_tokens: t.uint64().nullable(), + }, +}); + +export const modelUsageTrend = defineEndpoint("model_usage_trend", { + description: "Weekly usage share per model over the trailing window", + params: { + source: p.string().optional("openrouter").describe("Snapshot source"), + weeks: p.int32().optional(8).describe("Number of trailing weeks"), + }, + nodes: [ + node({ + name: "weekly_share", + sql: ` + SELECT + toMonday(captured_at) AS week, + model, + avg(share) AS avg_share, + max(raw_tokens) AS peak_tokens + FROM model_usage_share + WHERE source = {{String(source, 'openrouter')}} + AND captured_at >= toMonday(now()) - toIntervalWeek({{Int32(weeks, 8)}}) + GROUP BY week, model + ORDER BY week ASC, avg_share DESC + `, + }), + ], + output: { + week: t.date(), + model: t.string(), + avg_share: t.float64(), + peak_tokens: t.uint64().nullable(), + }, +}); + +export type ModelUsageLatestParams = InferParams; +export type ModelUsageLatestRow = InferOutputRow; +export type ModelUsageTrendParams = InferParams; +export type ModelUsageTrendRow = InferOutputRow; + +export type NotraAdoptionRow = InferOutputRow; +export type GeoOverviewRow = InferOutputRow; +export type GeoTimeseriesRow = InferOutputRow; +export type GeoPromptResultsRow = InferOutputRow; +export type GeoCompetitorShareRow = InferOutputRow; +export type GeoLanguageShareRow = InferOutputRow; + +export const postMetricsLookup = defineEndpoint("post_metrics_lookup", { + description: "Latest metric snapshot for specific posts by platform post id", + params: { + organization_id: p.string().describe("Organization id"), + post_ids: p.array(p.string()).describe("Platform post ids"), + }, + nodes: [ + node({ + name: "stats_lookup", + sql: ` + SELECT + provider, + platform_post_id, + argMaxMerge(impressions_state) AS impressions, + argMaxMerge(likes_state) AS likes, + argMaxMerge(replies_state) AS replies, + argMaxMerge(reposts_state) AS reposts, + argMaxMerge(bookmarks_state) AS bookmarks, + maxMerge(last_captured_at_state) AS last_captured_at + FROM social_post_stats_latest + WHERE organization_id = {{String(organization_id)}} + AND platform_post_id IN {{Array(post_ids, 'String')}} + GROUP BY provider, platform_post_id + `, + }), + node({ + name: "lookup", + sql: ` + SELECT + posts.provider AS provider, + posts.platform_post_id AS platform_post_id, + argMax(posts.content, posts.captured_at) AS content, + argMax(posts.url, posts.captured_at) AS url, + min(posts.posted_at) AS first_posted_at, + any(stats.impressions) AS impressions, + any(stats.likes) AS likes, + any(stats.replies) AS replies, + any(stats.reposts) AS reposts, + any(stats.bookmarks) AS bookmarks, + any(stats.last_captured_at) AS last_captured_at + FROM social_posts AS posts + LEFT JOIN stats_lookup AS stats + ON stats.provider = posts.provider + AND stats.platform_post_id = posts.platform_post_id + WHERE posts.organization_id = {{String(organization_id)}} + AND posts.platform_post_id IN {{Array(post_ids, 'String')}} + GROUP BY posts.provider, posts.platform_post_id + `, + }), + ], + output: { + provider: t.string(), + platform_post_id: t.string(), + content: t.string(), + url: t.string().nullable(), + first_posted_at: t.dateTime(), + impressions: t.uint64().nullable(), + likes: t.uint64().nullable(), + replies: t.uint64().nullable(), + reposts: t.uint64().nullable(), + bookmarks: t.uint64().nullable(), + last_captured_at: t.dateTime().nullable(), + }, +}); + +export type PostMetricsLookupRow = InferOutputRow; + +export const aiTrafficOverview = defineEndpoint("ai_traffic_overview", { + description: "AI agent hits per agent over the trailing window", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "per_agent", + sql: ` + SELECT + agent, + any(category) AS category, + any(confidence) AS confidence, + count() AS hits, + uniqExact(path) AS paths, + max(captured_at) AS last_seen_at + FROM ai_traffic_events + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + GROUP BY agent + ORDER BY hits DESC, agent ASC + `, + }), + ], + output: { + agent: t.string(), + category: t.string(), + confidence: t.string(), + hits: t.uint64(), + paths: t.uint64(), + last_seen_at: t.dateTime(), + }, +}); + +export const aiTrafficTimeseries = defineEndpoint("ai_traffic_timeseries", { + description: "Daily AI agent hits per category", + params: { + organization_id: p.string().describe("Organization id"), + days: p.int32().optional(30).describe("Number of trailing days"), + }, + nodes: [ + node({ + name: "daily", + sql: ` + SELECT + toDate(captured_at) AS day, + category, + count() AS hits + FROM ai_traffic_events + WHERE organization_id = {{String(organization_id)}} + AND captured_at >= now() - toIntervalDay({{Int32(days, 30)}}) + GROUP BY day, category + ORDER BY day ASC, category ASC + `, + }), + ], + output: { + day: t.date(), + category: t.string(), + hits: t.uint64(), + }, +}); + +export const aiTrafficLog = defineEndpoint("ai_traffic_log", { + description: "Most recent individual AI agent requests, newest first", + params: { + organization_id: p.string().describe("Organization id"), + limit: p.int32().optional(50).describe("Max events"), + }, + nodes: [ + node({ + name: "recent", + sql: ` + SELECT + captured_at, + agent, + category, + confidence, + path, + method, + referer + FROM ai_traffic_events + WHERE organization_id = {{String(organization_id)}} + ORDER BY captured_at DESC + LIMIT {{Int32(limit, 50)}} + `, + }), + ], + output: { + captured_at: t.dateTime(), + agent: t.string(), + category: t.string(), + confidence: t.string(), + path: t.string(), + method: t.string(), + referer: t.string().nullable(), + }, +}); + +export type AiTrafficOverviewRow = InferOutputRow; +export type AiTrafficTimeseriesRow = InferOutputRow; +export type AiTrafficLogRow = InferOutputRow; 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/analytics/src/utils/datetime.ts b/packages/analytics/src/utils/datetime.ts new file mode 100644 index 000000000..c0ed92a08 --- /dev/null +++ b/packages/analytics/src/utils/datetime.ts @@ -0,0 +1,13 @@ +const PAD_LENGTH = 2; + +function pad(value: number): string { + return String(value).padStart(PAD_LENGTH, "0"); +} + +export function toClickHouseDateTime(date: Date): string { + return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad( + date.getUTCDate() + )} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad( + date.getUTCSeconds() + )}`; +} diff --git a/packages/analytics/tinybird.config.json b/packages/analytics/tinybird.config.json new file mode 100644 index 000000000..59b8dbf35 --- /dev/null +++ b/packages/analytics/tinybird.config.json @@ -0,0 +1,6 @@ +{ + "include": ["src/tinybird/datasources.ts", "src/tinybird/endpoints.ts"], + "token": "${TINYBIRD_TOKEN}", + "baseUrl": "https://api.us-east.aws.tinybird.co", + "devMode": "branch" +} diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json new file mode 100644 index 000000000..e4ee95df2 --- /dev/null +++ b/packages/analytics/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@notra/typescript-config/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "strictNullChecks": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/beacon/README.md b/packages/beacon/README.md new file mode 100644 index 000000000..b3963da46 --- /dev/null +++ b/packages/beacon/README.md @@ -0,0 +1,84 @@ +# @notra/beacon + +Edge-safe detection of AI-agent traffic for Next.js. Classifies incoming requests +against a sourced signature table and reports matches to an ingest endpoint without +ever blocking or failing the request. + +Zero dependencies. No Node APIs, so it runs in middleware on the edge runtime. + +## Install + +```bash +bun add @notra/beacon +``` + +## Usage + +```ts +// middleware.ts (proxy.ts on Next.js 16) +import { createBeaconMiddleware } from "@notra/beacon/middleware"; +import { NextResponse } from "next/server"; + +const beacon = createBeaconMiddleware({ + ingestUrl: "https://app.usenotra.com/api/beacon", + token: process.env.BEACON_ORG_TOKEN ?? "", + organizationId: process.env.BEACON_ORG_ID ?? "", +}); + +export function middleware(request: Request) { + beacon(request); + return NextResponse.next(); +} +``` + +`beacon(request)` returns the `BeaconMatch` it detected (or `null`) and schedules the +report as a side effect. Pass a context with `waitUntil` as the second argument when +you have one, so the report survives the response. + +## API + +### `classifyRequest(headers: Headers, ip?: string): BeaconMatch | null` + +Pure. Case-insensitive substring match of the `User-Agent` header against +`BEACON_SIGNATURES`. Returns `{ agent, vendor, category, confidence }` or `null`. The +`ip` argument is accepted for forward compatibility with IP-range verification and is +currently unused. + +### `classifyUserAgent(userAgent: string): BeaconMatch | null` + +The same match against a raw string. + +### `createBeaconMiddleware(config): (request, context?) => BeaconMatch | null` + +Config: + +| Field | Required | Meaning | +| --- | --- | --- | +| `ingestUrl` | yes | Where to POST the event | +| `token` | yes | Per-organization ingest token | +| `organizationId` | yes | Organization the hit belongs to | +| `sample` | no | 0..1 sample rate, default 1 | +| `fetchImpl` | no | Injectable `fetch`, for tests | + +### `reportAiHit(config, event, context?): void` + +Fire-and-forget POST for manual use. Never throws. + +### Categories + +- `training-crawler` — collects pages for model training corpora +- `search-index` — builds the index an AI answer engine searches +- `assistant-browse` — fetched while an assistant was answering someone + +An `assistant-browse` hit means a page was fetched during an answer. It is not proof +the page was cited. + +## Signature provenance + +Every signature carries a `source` URL and a `confidence` of `verified`, `reported`, or +`heuristic`. See [SOURCES.md](./SOURCES.md), which also documents the agents that are +deliberately **not** in the table because they cannot be honestly detected by +user-agent, including Cursor, ChatGPT Atlas, `Google-Extended` and `Applebot-Extended`. + +User-agent matching is spoofable. The `verification` field on each signature points at +the operator's published IP-range list or reverse-DNS method where one exists. diff --git a/packages/beacon/SOURCES.md b/packages/beacon/SOURCES.md new file mode 100644 index 000000000..98dc8c591 --- /dev/null +++ b/packages/beacon/SOURCES.md @@ -0,0 +1,261 @@ +# Signature sources + +Every entry in `src/signatures.ts` is listed here with the source it came from and a +confidence level. Nothing in the table is invented. If a vendor does not publish a +user-agent, the agent is either absent from the table or documented below as +unmatchable. + +Confidence levels: + +- **verified** — the operator publishes the token in its own documentation. +- **reported** — the token appears only in credible third-party catalogs, controlled + header tests, or server-log studies. Spoofable and unconfirmed by the vendor. +- **heuristic** — the token is real but our category assignment is an inference. + +Matching is a case-insensitive substring test on the `User-Agent` header only. UA +matching is trivially spoofable; the `verification` field points at the published IP +range list or reverse-DNS method that can prove a hit is genuine. Nothing in this +package performs that verification yet. + +## OpenAI + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| GPTBot | training-crawler | `GPTBot` | verified | +| OAI-SearchBot | search-index | `OAI-SearchBot` | verified | +| ChatGPT-User | assistant-browse | `ChatGPT-User` | verified | +| OAI-AdsBot | search-index | `OAI-AdsBot` | verified | + +Source: . IP ranges: +, , +, . Each file has +the shape `{"creationTime": ISO8601, "prefixes": [{"ipv4Prefix": "..."}]}`. + +OAI-AdsBot is filed under `search-index` because our category enum has no ad-safety +bucket; OpenAI describes it as a landing-page checker, not a training crawler. + +There is **no** `ChatGPT-Agent` or `Operator` user-agent token. ChatGPT Atlas and the +ChatGPT agent send an ordinary Chrome user-agent and identify themselves with Web Bot +Auth HTTP message signatures instead +(). They cannot be +detected by user-agent and are deliberately absent from the table. + +## Anthropic + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| ClaudeBot | training-crawler | `ClaudeBot` | verified | +| Claude-User | assistant-browse | `Claude-User` | verified | +| Claude-SearchBot | search-index | `Claude-SearchBot` | verified | +| Claude Code | assistant-browse | `claude-code/` | reported | +| anthropic-ai | training-crawler | `anthropic-ai` | reported (legacy) | +| claude-web | assistant-browse | `claude-web` | reported (legacy) | + +Source: . IP ranges: +. + +Anthropic documents the three tokens but publishes **no** full user-agent strings, so +we match on the token only. `anthropic-ai` and `claude-web` are legacy names that +appear only in ; that catalog itself +notes there is no official documentation for `Claude-Web`. + +Claude Code sends `Claude-User (claude-code/; +https://support.anthropic.com/)` +in newer builds (); older builds sent +a bare `axios/1.8.4`, which is not attributable. The `claude-code/` entry is ordered +before `Claude-User` so Claude Code hits are labelled specifically. + +## Perplexity + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| PerplexityBot | search-index | `PerplexityBot` | verified | +| Perplexity-User | assistant-browse | `Perplexity-User` | verified | + +Source: . IP ranges: +, +. + +Perplexity has been documented fetching from undeclared user-agents and IPs, so a UA +miss does not mean Perplexity did not visit. + +## Google + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| Google-CloudVertexBot | training-crawler | `Google-CloudVertexBot` | verified | +| GoogleOther-Image | training-crawler | `GoogleOther-Image` | verified | +| GoogleOther-Video | training-crawler | `GoogleOther-Video` | verified | +| GoogleOther | training-crawler | `GoogleOther` | heuristic | +| GoogleAgent-URLContext | assistant-browse | `GoogleAgent-URLContext`, `Google-Gemini-CLI` | reported | + +Sources: +, +. +IP ranges: `https://developers.google.com/static/search/apis/ipranges/special-crawlers.json` +plus reverse DNS in `*.googlebot.com` / `*.google.com`. + +**`Google-Extended` is deliberately absent.** Google states it "doesn't have a separate +HTTP request user agent string" — it is a robots.txt control token for Gemini and Vertex +AI grounding only. Any request whose UA literally contains `Google-Extended` is spoofed, +so matching it would produce fake data. + +`GoogleOther` is marked heuristic: the token is verified, but Google describes it as a +shared one-off crawl pool for internal R&D rather than a dedicated AI training crawler, +so the `training-crawler` category is our inference. + +`GoogleAgent-URLContext` is the Gemini CLI / URL-context fetcher, observed in a +controlled header test () +rather than documented by Google. + +Plain `Googlebot` is not in the table: it is classic search indexing, not AI traffic. + +## Apple + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| Applebot | search-index | `Applebot/` | verified | + +Source: . IP ranges: +, plus forward-confirmed reverse DNS +in `*.applebot.apple.com`. + +**`Applebot-Extended` is deliberately absent.** Apple states it "does not crawl +webpages... only used to determine how to use the data crawled by the Applebot user +agent". It is a robots.txt directive, not a fetcher. The token is matched with a +trailing slash (`Applebot/`) because Apple's real UA reads `(Applebot/0.1; +...)`. + +## Meta + +| Agent | Category | Token | Confidence | +| --- | --- | --- | --- | +| meta-externalagent | training-crawler | `meta-externalagent` | verified | +| meta-externalfetcher | assistant-browse | `meta-externalfetcher` | verified | +| FacebookBot | training-crawler | `FacebookBot` | reported | + +Source: . Meta +publishes no IP list, so none of these can be verified. `FacebookBot` is not on Meta's +crawler page and comes from ai.robots.txt only. + +## Amazon, DuckDuckGo, Liner, You.com, Mistral, Common Crawl + +| Agent | Vendor | Category | Token | Confidence | Verification | +| --- | --- | --- | --- | --- | --- | +| Amazonbot | Amazon | training-crawler | `Amazonbot` | verified | | +| DuckAssistBot | DuckDuckGo | assistant-browse | `DuckAssistBot` | verified | | +| LinerBot | Liner | search-index | `LinerBot` | verified | | +| YouBot | You.com | search-index | `YouBot` | verified | rDNS `*.search.you.com`, `68.67.112.0/24` | +| MistralAI-User | Mistral | assistant-browse | `MistralAI-User` | verified | | +| CCBot | Common Crawl | training-crawler | `CCBot` | verified | | + +Docs: , +, +, , +, . + +## Unverifiable but widely observed + +| Agent | Vendor | Category | Token | Confidence | +| --- | --- | --- | --- | --- | +| Bytespider | ByteDance | training-crawler | `Bytespider` | reported | +| cohere-training-data-crawler | Cohere | training-crawler | `cohere-training-data-crawler` | reported | +| cohere-ai | Cohere | assistant-browse | `cohere-ai` | reported | +| Timpibot | Timpi | training-crawler | `Timpibot` | reported | +| Diffbot | Diffbot | training-crawler | `Diffbot` | reported | +| omgili | Webz.io | training-crawler | `omgilibot`, `omgili` | verified token | +| Devin | Cognition | assistant-browse | `Devin/` | reported | +| Trae-Agent | ByteDance | assistant-browse | `Trae-Agent` | reported | + +ByteDance, Cohere and Timpi publish no crawler documentation at all; the tokens come +from . Cohere's `docs.cohere.com/docs/crawling` +returns 404. Diffbot confirms the `Diffbot` and `Diffbot-User` robots.txt tokens +() but publishes no UA string. +Webz.io documents `omgili`/`omgilibot` +(). +Devin appears in Cloudflare's verified-bots directory +() but not in Cognition's own docs. +Trae comes from . + +Bytespider is widely reported to ignore robots.txt. None of these can be verified by +IP, so a UA match is a hint, not proof. + +## Cursor: not fingerprintable by user-agent + +**Cursor is deliberately absent from `signatures.ts`.** We could not find a Cursor +user-agent token that would be honest to ship. The findings, with confidence: + +- **Cursor sends a stock Chrome user-agent** with no vendor token. A controlled + header-echo test of Cursor 2.4.28's native WebFetch against `httpbin.org/headers` + recorded `Mozilla/5.0 ... Chrome/139.0.0.0 Safari/537.36` plus Chrome client hints + (`Sec-Ch-Ua`, `Sec-Fetch-*`) and + `Accept: text/markdown,text/html;q=0.9,...` + () — **reported**. + Independently corroborated by + . +- **Cursor also routes some fetches through generic clients.** The same study found + Cursor falling back to Python `urllib` and `curl` for larger payloads, so some hits + arrive as `python-urllib/x.y` or `curl/x.y` — **reported**. Those are shared by + thousands of unrelated scripts and are useless as an attribution signal. +- **Cursor publishes no user-agent.** Cursor staff confirmed no public server + identifiers exist + () + — **verified absence**. +- **Do not trust the catalog entry.** lists + Cursor with the user-agent `IntelFeed-People/0.1 (+https://github.com/bertchen321wehc/Cursor)` + and operator "xAI". That is a corrupted submission pointing at an unrelated personal + GitHub repository, and it has propagated verbatim into `ai-robots-txt/ai.robots.txt`. + Shipping it would produce false positives. +- **Most Cursor `@Web` traffic never reaches your origin.** Cursor's own security page + names Exa and SerpApi as its search providers, so page content is usually served from + Exa's index rather than crawled live + () — + **verified**. Exa publishes no crawler user-agent or IP list. + +The closest usable signals, none of which we implement: + +1. **Cloud Agent IP ranges — verified.** lists 416 + `/32` egress addresses plus three git-egress proxy IPs, all inside AWS us-east-1 / + us-east-2 / us-west-2 prefixes (docs: + ). This is the only + Cursor-attributable network signal, and it covers Cloud Agents only — not the + desktop IDE. There is no Anysphere ASN; the address space is Amazon's. +2. **Header heuristic — heuristic, not Cursor-specific.** "Chrome desktop UA + + `Accept: text/markdown` with a q-factor chain + no JavaScript execution" flags + markdown-negotiating coding agents as a class. OpenCode matches the same shape, so + it identifies "a coding agent", not "Cursor". +3. **`x-cursor-client-version` / `x-cursor-client-type` headers — reported.** These are + sent only to Cursor's own API and never reach third-party origins. + +**Bottom line: there is no reliable way to detect Cursor from a web request today.** A +44-day Cloudflare log study reached the same conclusion, finding markdown-requesting +"standard Chrome" traffic from headless pools that could not be attributed to any +vendor (). If Cursor ships +a real token we will add it; until then the table stays honest and leaves it out. + +## Other coding agents we checked and left out + +- **GitHub Copilot** — sends a VS Code Electron UA (`Code/1.109.3 ... Electron/39.3.0`) + or `curl/8.7.1`; no documented fetch token. There *is* an official IP signal: the + `copilot` key in (17 CIDRs) — **verified**. UA-only + detection is impossible. +- **Windsurf / Codeium** — observed sending + `colly - https://github.com/gocolly/colly`, a generic Go scraping library UA shared + by unrelated scrapers — **reported**, too generic to ship. +- **Cline** — open source; its SDK web-fetch defaults to + `Mozilla/5.0 (compatible; AgentBot/1.0)`, link previews use + `Mozilla/5.0 (compatible; VSCodeExtension/1.0; +https://cline.bot)`, and Puppeteer + browsing spoofs Chrome 128 () — **verified from + source**, but the tokens are generic enough to cause false positives. +- **Replit** — no published user-agent or IP list found in any catalog or in Replit's + docs — **verified absence**. +- **OpenAI Codex** — reuses `ChatGPT-User`, already covered. + +## Catalog caveats + +- `darkvisitors.com/agents` now redirects to `knownagents.com`. Its data quality is + mixed (see the Cursor entry above); treat it as a lead, not a source. +- `ai-robots-txt/ai.robots.txt` contains robots.txt tokens, not user-agent strings, and + re-imports knownagents entries verbatim including bad ones. +- Cloudflare lists three accepted bot-verification mechanisms: Web Bot Auth, published + IP ranges with stable user-agents, and reverse DNS + (). User-agent + matching alone is none of them. diff --git a/packages/beacon/package.json b/packages/beacon/package.json new file mode 100644 index 000000000..7401fca25 --- /dev/null +++ b/packages/beacon/package.json @@ -0,0 +1,33 @@ +{ + "name": "@notra/beacon", + "version": "0.0.1", + "private": false, + "type": "module", + "description": "Edge-safe Next.js middleware that detects and reports AI agent traffic", + "license": "AGPL-3.0", + "sideEffects": false, + "files": [ + "src", + "SOURCES.md", + "README.md" + ], + "exports": { + "./*": "./src/*.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + } + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@notra/typescript-config": "workspace:*", + "typescript": "5.9.2" + } +} diff --git a/packages/beacon/src/classify.ts b/packages/beacon/src/classify.ts new file mode 100644 index 000000000..92b9f85b3 --- /dev/null +++ b/packages/beacon/src/classify.ts @@ -0,0 +1,31 @@ +import { BEACON_SIGNATURES } from "./signatures"; +import type { BeaconMatch } from "./types"; + +export function classifyUserAgent(userAgent: string): BeaconMatch | null { + const haystack = userAgent.toLowerCase(); + if (!haystack) { + return null; + } + + for (const signature of BEACON_SIGNATURES) { + for (const token of signature.userAgents) { + if (haystack.includes(token.toLowerCase())) { + return { + agent: signature.agent, + vendor: signature.vendor, + category: signature.category, + confidence: signature.confidence, + }; + } + } + } + + return null; +} + +export function classifyRequest( + headers: Headers, + _ip?: string +): BeaconMatch | null { + return classifyUserAgent(headers.get("user-agent") ?? ""); +} diff --git a/packages/beacon/src/middleware.ts b/packages/beacon/src/middleware.ts new file mode 100644 index 000000000..28ce314e2 --- /dev/null +++ b/packages/beacon/src/middleware.ts @@ -0,0 +1,88 @@ +import { classifyRequest } from "./classify"; +import { reportAiHit } from "./report"; +import type { + BeaconConfig, + BeaconEvent, + BeaconEventContext, + BeaconMatch, + BeaconRequestLike, +} from "./types"; + +const MAX_UA_LENGTH = 512; +const MAX_PATH_LENGTH = 512; +const MAX_REFERER_LENGTH = 512; + +function truncate(value: string, max: number): string { + return value.length > max ? value.slice(0, max) : value; +} + +function shouldSample(sample: number | undefined): boolean { + if (sample === undefined || sample >= 1) { + return true; + } + if (sample <= 0) { + return false; + } + return Math.random() < sample; +} + +function buildEvent( + config: BeaconConfig, + request: BeaconRequestLike, + match: BeaconMatch +): BeaconEvent | null { + let url: URL; + try { + url = new URL(request.url); + } catch { + return null; + } + + const referer = request.headers.get("referer"); + + return { + token: config.token, + organizationId: config.organizationId, + agent: match.agent, + category: match.category, + confidence: match.confidence, + path: truncate(url.pathname, MAX_PATH_LENGTH), + host: request.headers.get("host") ?? url.host, + method: request.method, + referer: referer ? truncate(referer, MAX_REFERER_LENGTH) : null, + ua: truncate(request.headers.get("user-agent") ?? "", MAX_UA_LENGTH), + ts: new Date().toISOString(), + }; +} + +export function trackAiRequest( + config: BeaconConfig, + request: BeaconRequestLike, + context?: BeaconEventContext +): BeaconMatch | null { + try { + const match = classifyRequest(request.headers); + if (!match) { + return null; + } + if (!shouldSample(config.sample)) { + return match; + } + + const event = buildEvent(config, request, match); + if (event) { + reportAiHit(config, event, context); + } + + return match; + } catch { + return null; + } +} + +export function createBeaconMiddleware(config: BeaconConfig) { + return ( + request: BeaconRequestLike, + context?: BeaconEventContext + ): BeaconMatch | null => trackAiRequest(config, request, context); +} diff --git a/packages/beacon/src/report.ts b/packages/beacon/src/report.ts new file mode 100644 index 000000000..69fac3896 --- /dev/null +++ b/packages/beacon/src/report.ts @@ -0,0 +1,34 @@ +import type { BeaconConfig, BeaconEvent, BeaconEventContext } from "./types"; + +const INGEST_TIMEOUT_MS = 2000; + +function schedule( + promise: Promise, + context: BeaconEventContext | undefined +): void { + const settled = promise.catch(() => undefined); + context?.waitUntil?.(settled); +} + +export function reportAiHit( + config: BeaconConfig, + event: BeaconEvent, + context?: BeaconEventContext +): void { + const send = config.fetchImpl ?? fetch; + + try { + schedule( + send(config.ingestUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(event), + keepalive: true, + signal: AbortSignal.timeout(INGEST_TIMEOUT_MS), + }), + context + ); + } catch { + return; + } +} diff --git a/packages/beacon/src/signatures.ts b/packages/beacon/src/signatures.ts new file mode 100644 index 000000000..69f6fa248 --- /dev/null +++ b/packages/beacon/src/signatures.ts @@ -0,0 +1,339 @@ +import type { BeaconSignature } from "./types"; + +export const BEACON_SIGNATURES: readonly BeaconSignature[] = [ + { + agent: "GPTBot", + vendor: "OpenAI", + category: "training-crawler", + userAgents: ["GPTBot"], + confidence: "verified", + verification: "https://openai.com/gptbot.json", + source: "https://developers.openai.com/api/docs/bots", + }, + { + agent: "OAI-SearchBot", + vendor: "OpenAI", + category: "search-index", + userAgents: ["OAI-SearchBot"], + confidence: "verified", + verification: "https://openai.com/searchbot.json", + source: "https://developers.openai.com/api/docs/bots", + }, + { + agent: "ChatGPT-User", + vendor: "OpenAI", + category: "assistant-browse", + userAgents: ["ChatGPT-User"], + confidence: "verified", + verification: "https://openai.com/chatgpt-user.json", + source: "https://developers.openai.com/api/docs/bots", + }, + { + agent: "OAI-AdsBot", + vendor: "OpenAI", + category: "search-index", + userAgents: ["OAI-AdsBot"], + confidence: "verified", + verification: "https://openai.com/adsbot.json", + source: "https://developers.openai.com/api/docs/bots", + }, + { + agent: "Claude Code", + vendor: "Anthropic", + category: "assistant-browse", + userAgents: ["claude-code/"], + confidence: "reported", + verification: null, + source: "https://github.com/monperrus/crawler-user-agents", + }, + { + agent: "Claude-SearchBot", + vendor: "Anthropic", + category: "search-index", + userAgents: ["Claude-SearchBot"], + confidence: "verified", + verification: "https://claude.com/crawling/bots.json", + source: "https://support.claude.com/en/articles/8896518", + }, + { + agent: "Claude-User", + vendor: "Anthropic", + category: "assistant-browse", + userAgents: ["Claude-User"], + confidence: "verified", + verification: "https://claude.com/crawling/bots.json", + source: "https://support.claude.com/en/articles/8896518", + }, + { + agent: "ClaudeBot", + vendor: "Anthropic", + category: "training-crawler", + userAgents: ["ClaudeBot"], + confidence: "verified", + verification: "https://claude.com/crawling/bots.json", + source: "https://support.claude.com/en/articles/8896518", + }, + { + agent: "anthropic-ai", + vendor: "Anthropic", + category: "training-crawler", + userAgents: ["anthropic-ai"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "claude-web", + vendor: "Anthropic", + category: "assistant-browse", + userAgents: ["claude-web"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "PerplexityBot", + vendor: "Perplexity", + category: "search-index", + userAgents: ["PerplexityBot"], + confidence: "verified", + verification: "https://www.perplexity.ai/perplexitybot.json", + source: "https://docs.perplexity.ai/guides/bots", + }, + { + agent: "Perplexity-User", + vendor: "Perplexity", + category: "assistant-browse", + userAgents: ["Perplexity-User"], + confidence: "verified", + verification: "https://www.perplexity.ai/perplexity-user.json", + source: "https://docs.perplexity.ai/guides/bots", + }, + { + agent: "Google-CloudVertexBot", + vendor: "Google", + category: "training-crawler", + userAgents: ["Google-CloudVertexBot"], + confidence: "verified", + verification: + "https://developers.google.com/static/search/apis/ipranges/special-crawlers.json", + source: + "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers", + }, + { + agent: "GoogleOther-Image", + vendor: "Google", + category: "training-crawler", + userAgents: ["GoogleOther-Image"], + confidence: "verified", + verification: + "https://developers.google.com/static/search/apis/ipranges/special-crawlers.json", + source: + "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers", + }, + { + agent: "GoogleOther-Video", + vendor: "Google", + category: "training-crawler", + userAgents: ["GoogleOther-Video"], + confidence: "verified", + verification: + "https://developers.google.com/static/search/apis/ipranges/special-crawlers.json", + source: + "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers", + }, + { + agent: "GoogleOther", + vendor: "Google", + category: "training-crawler", + userAgents: ["GoogleOther"], + confidence: "heuristic", + verification: + "https://developers.google.com/static/search/apis/ipranges/special-crawlers.json", + source: + "https://developers.google.com/search/docs/crawling-indexing/google-special-case-crawlers", + }, + { + agent: "Bytespider", + vendor: "ByteDance", + category: "training-crawler", + userAgents: ["Bytespider"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "Amazonbot", + vendor: "Amazon", + category: "training-crawler", + userAgents: ["Amazonbot"], + confidence: "verified", + verification: "https://developer.amazon.com/amazonbot/ip-addresses/", + source: "https://developer.amazon.com/amazonbot", + }, + { + agent: "Applebot", + vendor: "Apple", + category: "search-index", + userAgents: ["Applebot/"], + confidence: "verified", + verification: "https://search.developer.apple.com/applebot.json", + source: "https://support.apple.com/en-us/119829", + }, + { + agent: "cohere-training-data-crawler", + vendor: "Cohere", + category: "training-crawler", + userAgents: ["cohere-training-data-crawler"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "cohere-ai", + vendor: "Cohere", + category: "assistant-browse", + userAgents: ["cohere-ai"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "meta-externalagent", + vendor: "Meta", + category: "training-crawler", + userAgents: ["meta-externalagent"], + confidence: "verified", + verification: null, + source: + "https://developers.facebook.com/docs/sharing/webmasters/web-crawlers/", + }, + { + agent: "meta-externalfetcher", + vendor: "Meta", + category: "assistant-browse", + userAgents: ["meta-externalfetcher"], + confidence: "verified", + verification: null, + source: + "https://developers.facebook.com/docs/sharing/webmasters/web-crawlers/", + }, + { + agent: "FacebookBot", + vendor: "Meta", + category: "training-crawler", + userAgents: ["FacebookBot"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "DuckAssistBot", + vendor: "DuckDuckGo", + category: "assistant-browse", + userAgents: ["DuckAssistBot"], + confidence: "verified", + verification: "https://duckduckgo.com/duckassistbot.json", + source: + "https://duckduckgo.com/duckduckgo-help-pages/results/duckassistbot/", + }, + { + agent: "LinerBot", + vendor: "Liner", + category: "search-index", + userAgents: ["LinerBot"], + confidence: "verified", + verification: "https://docs.getliner.com/linerbot.json", + source: "https://docs.getliner.com/docs/linerbot", + }, + { + agent: "YouBot", + vendor: "You.com", + category: "search-index", + userAgents: ["YouBot"], + confidence: "verified", + verification: "https://you.com/docs/youbot", + source: "https://you.com/docs/youbot", + }, + { + agent: "MistralAI-User", + vendor: "Mistral", + category: "assistant-browse", + userAgents: ["MistralAI-User"], + confidence: "verified", + verification: "https://mistral.ai/mistralai-user-ips.json", + source: "https://docs.mistral.ai/robots", + }, + { + agent: "CCBot", + vendor: "Common Crawl", + category: "training-crawler", + userAgents: ["CCBot"], + confidence: "verified", + verification: "https://index.commoncrawl.org/ccbot.json", + source: "https://commoncrawl.org/ccbot", + }, + { + agent: "Diffbot", + vendor: "Diffbot", + category: "training-crawler", + userAgents: ["Diffbot"], + confidence: "reported", + verification: null, + source: "https://www.diffbot.com/docs/crawl/faq/robots-txt", + }, + { + agent: "Timpibot", + vendor: "Timpi", + category: "training-crawler", + userAgents: ["Timpibot"], + confidence: "reported", + verification: null, + source: + "https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.json", + }, + { + agent: "Devin", + vendor: "Cognition", + category: "assistant-browse", + userAgents: ["Devin/"], + confidence: "reported", + verification: null, + source: "https://radar.cloudflare.com/traffic/verified-bots", + }, + { + agent: "GoogleAgent-URLContext", + vendor: "Google", + category: "assistant-browse", + userAgents: ["GoogleAgent-URLContext", "Google-Gemini-CLI"], + confidence: "reported", + verification: null, + source: + "https://www.checklyhq.com/blog/state-of-ai-agent-content-negotation/", + }, + { + agent: "Trae-Agent", + vendor: "ByteDance", + category: "assistant-browse", + userAgents: ["Trae-Agent"], + confidence: "reported", + verification: null, + source: "https://knownagents.com/agents/trae", + }, + { + agent: "omgili", + vendor: "Webz.io", + category: "training-crawler", + userAgents: ["omgilibot", "omgili"], + confidence: "verified", + verification: null, + source: + "https://webz.io/blog/web-data/what-is-the-omgili-bot-and-why-is-it-crawling-your-website/", + }, +]; diff --git a/packages/beacon/src/types.ts b/packages/beacon/src/types.ts new file mode 100644 index 000000000..40bd60c86 --- /dev/null +++ b/packages/beacon/src/types.ts @@ -0,0 +1,55 @@ +export type BeaconCategory = + | "training-crawler" + | "search-index" + | "assistant-browse"; + +export type BeaconConfidence = "verified" | "reported" | "heuristic"; + +export interface BeaconSignature { + agent: string; + vendor: string; + category: BeaconCategory; + userAgents: string[]; + confidence: BeaconConfidence; + verification: string | null; + source: string; +} + +export interface BeaconMatch { + agent: string; + vendor: string; + category: BeaconCategory; + confidence: BeaconConfidence; +} + +export interface BeaconEvent { + token: string; + organizationId: string; + agent: string; + category: BeaconCategory; + confidence: BeaconConfidence; + path: string; + host: string; + method: string; + referer: string | null; + ua: string; + ts: string; +} + +export interface BeaconConfig { + ingestUrl: string; + token: string; + organizationId: string; + sample?: number; + fetchImpl?: typeof fetch; +} + +export interface BeaconRequestLike { + headers: Headers; + method: string; + url: string; +} + +export interface BeaconEventContext { + waitUntil?: (promise: Promise) => void; +} diff --git a/packages/beacon/tsconfig.json b/packages/beacon/tsconfig.json new file mode 100644 index 000000000..454564884 --- /dev/null +++ b/packages/beacon/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@notra/typescript-config/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "strictNullChecks": true, + "types": [] + }, + "include": ["src"], + "exclude": ["node_modules"] +} 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/0063_slimy_hellcat.sql b/packages/db/migrations/0063_slimy_hellcat.sql new file mode 100644 index 000000000..a6029f6df --- /dev/null +++ b/packages/db/migrations/0063_slimy_hellcat.sql @@ -0,0 +1,19 @@ +CREATE TABLE "social_experiments" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "name" text NOT NULL, + "hypothesis" text, + "provider" text NOT NULL, + "variant_a_post_id" text NOT NULL, + "variant_b_post_id" text NOT NULL, + "metric" text NOT NULL, + "status" text DEFAULT 'running' NOT NULL, + "winner" text, + "started_at" timestamp DEFAULT now() NOT NULL, + "ended_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "social_experiments" ADD CONSTRAINT "social_experiments_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "socialExperiments_organizationId_idx" ON "social_experiments" USING btree ("organization_id"); \ No newline at end of file diff --git a/packages/db/migrations/0064_exotic_scourge.sql b/packages/db/migrations/0064_exotic_scourge.sql new file mode 100644 index 000000000..b809a0f09 --- /dev/null +++ b/packages/db/migrations/0064_exotic_scourge.sql @@ -0,0 +1,24 @@ +CREATE TABLE "geo_prompts" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "prompt" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "geo_settings" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "company_name" text NOT NULL, + "aliases" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "competitors" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "geo_prompts" ADD CONSTRAINT "geo_prompts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "geo_settings" ADD CONSTRAINT "geo_settings_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "geoPrompts_organizationId_idx" ON "geo_prompts" USING btree ("organization_id");--> statement-breakpoint +CREATE UNIQUE INDEX "geoSettings_organizationId_uidx" ON "geo_settings" USING btree ("organization_id"); \ No newline at end of file diff --git a/packages/db/migrations/0065_cool_prism.sql b/packages/db/migrations/0065_cool_prism.sql new file mode 100644 index 000000000..e6b19b33d --- /dev/null +++ b/packages/db/migrations/0065_cool_prism.sql @@ -0,0 +1 @@ +ALTER TABLE "geo_settings" ADD COLUMN "languages" text[]; \ 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/0063_snapshot.json b/packages/db/migrations/meta/0063_snapshot.json new file mode 100644 index 000000000..f3d80b345 --- /dev/null +++ b/packages/db/migrations/meta/0063_snapshot.json @@ -0,0 +1,9126 @@ +{ + "id": "3e7d6177-419d-4b9f-ac44-8ddda86c56de", + "prevId": "15722568-6fad-4c25-a22b-220cdc45d546", + "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.social_experiments": { + "name": "social_experiments", + "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 + }, + "hypothesis": { + "name": "hypothesis", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_a_post_id": { + "name": "variant_a_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_b_post_id": { + "name": "variant_b_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_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": { + "socialExperiments_organizationId_idx": { + "name": "socialExperiments_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "social_experiments_organization_id_organizations_id_fk": { + "name": "social_experiments_organization_id_organizations_id_fk", + "tableFrom": "social_experiments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_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/0064_snapshot.json b/packages/db/migrations/meta/0064_snapshot.json new file mode 100644 index 000000000..33aecd97a --- /dev/null +++ b/packages/db/migrations/meta/0064_snapshot.json @@ -0,0 +1,9304 @@ +{ + "id": "5565d305-8e7a-47db-8672-c03fa92c9ebf", + "prevId": "3e7d6177-419d-4b9f-ac44-8ddda86c56de", + "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.geo_prompts": { + "name": "geo_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "geoPrompts_organizationId_idx": { + "name": "geoPrompts_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "geo_prompts_organization_id_organizations_id_fk": { + "name": "geo_prompts_organization_id_organizations_id_fk", + "tableFrom": "geo_prompts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.geo_settings": { + "name": "geo_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "competitors": { + "name": "competitors", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "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": { + "geoSettings_organizationId_uidx": { + "name": "geoSettings_organizationId_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "geo_settings_organization_id_organizations_id_fk": { + "name": "geo_settings_organization_id_organizations_id_fk", + "tableFrom": "geo_settings", + "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.social_experiments": { + "name": "social_experiments", + "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 + }, + "hypothesis": { + "name": "hypothesis", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_a_post_id": { + "name": "variant_a_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_b_post_id": { + "name": "variant_b_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_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": { + "socialExperiments_organizationId_idx": { + "name": "socialExperiments_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "social_experiments_organization_id_organizations_id_fk": { + "name": "social_experiments_organization_id_organizations_id_fk", + "tableFrom": "social_experiments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_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/0065_snapshot.json b/packages/db/migrations/meta/0065_snapshot.json new file mode 100644 index 000000000..756c7b128 --- /dev/null +++ b/packages/db/migrations/meta/0065_snapshot.json @@ -0,0 +1,9310 @@ +{ + "id": "2c7b2ce0-54f1-4316-9ef9-7ab2b9d1b1c8", + "prevId": "5565d305-8e7a-47db-8672-c03fa92c9ebf", + "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.geo_prompts": { + "name": "geo_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "geoPrompts_organizationId_idx": { + "name": "geoPrompts_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "geo_prompts_organization_id_organizations_id_fk": { + "name": "geo_prompts_organization_id_organizations_id_fk", + "tableFrom": "geo_prompts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.geo_settings": { + "name": "geo_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "competitors": { + "name": "competitors", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "languages": { + "name": "languages", + "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": { + "geoSettings_organizationId_uidx": { + "name": "geoSettings_organizationId_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "geo_settings_organization_id_organizations_id_fk": { + "name": "geo_settings_organization_id_organizations_id_fk", + "tableFrom": "geo_settings", + "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.social_experiments": { + "name": "social_experiments", + "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 + }, + "hypothesis": { + "name": "hypothesis", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_a_post_id": { + "name": "variant_a_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant_b_post_id": { + "name": "variant_b_post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_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": { + "socialExperiments_organizationId_idx": { + "name": "socialExperiments_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "social_experiments_organization_id_organizations_id_fk": { + "name": "social_experiments_organization_id_organizations_id_fk", + "tableFrom": "social_experiments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_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..80eca0974 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -435,6 +435,34 @@ "when": 1785580721087, "tag": "0061_lazy_dreadnoughts", "breakpoints": true + }, + { + "idx": 62, + "version": "7", + "when": 1785669058664, + "tag": "0062_brave_menace", + "breakpoints": true + }, + { + "idx": 63, + "version": "7", + "when": 1785669117308, + "tag": "0063_slimy_hellcat", + "breakpoints": true + }, + { + "idx": 64, + "version": "7", + "when": 1785669176828, + "tag": "0064_exotic_scourge", + "breakpoints": true + }, + { + "idx": 65, + "version": "7", + "when": 1785706877007, + "tag": "0065_cool_prism", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c5fccd2da..3bbca1b1c 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", { @@ -1422,6 +1452,78 @@ export const organizationNotificationSettings = pgTable( ] ); +export const geoSettings = pgTable( + "geo_settings", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + companyName: text("company_name").notNull(), + aliases: text("aliases").array().notNull().default(sql`ARRAY[]::text[]`), + competitors: text("competitors") + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), + languages: text("languages").array(), + enabled: boolean("enabled").notNull().default(true), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex("geoSettings_organizationId_uidx").on(table.organizationId), + ] +); + +export const geoPrompts = pgTable( + "geo_prompts", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + prompt: text("prompt").notNull(), + enabled: boolean("enabled").notNull().default(true), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("geoPrompts_organizationId_idx").on(table.organizationId)] +); + +export const socialExperiments = pgTable( + "social_experiments", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + name: text("name").notNull(), + hypothesis: text("hypothesis"), + provider: text("provider").notNull(), + variantAPostId: text("variant_a_post_id").notNull(), + variantBPostId: text("variant_b_post_id").notNull(), + metric: text("metric").notNull(), + status: text("status").notNull().default("running"), + winner: text("winner"), + startedAt: timestamp("started_at").defaultNow().notNull(), + endedAt: timestamp("ended_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + index("socialExperiments_organizationId_idx").on(table.organizationId), + ] +); + export const postCollections = pgTable( "post_collections", { @@ -2056,6 +2158,8 @@ export const organizationsRelations = relations( mcpSessionToolActivations: many(mcpSessionToolActivations), brandSettings: many(brandSettings), notificationSettings: one(organizationNotificationSettings), + geoSettings: one(geoSettings), + geoPrompts: many(geoPrompts), connectedSocialAccounts: many(connectedSocialAccounts), postCollections: many(postCollections), posts: many(posts), @@ -2416,6 +2520,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 }) => ({ @@ -2426,6 +2540,30 @@ export const organizationNotificationSettingsRelations = relations( }) ); +export const geoSettingsRelations = relations(geoSettings, ({ one }) => ({ + organization: one(organizations, { + fields: [geoSettings.organizationId], + references: [organizations.id], + }), +})); + +export const socialExperimentsRelations = relations( + socialExperiments, + ({ one }) => ({ + organization: one(organizations, { + fields: [socialExperiments.organizationId], + references: [organizations.id], + }), + }) +); + +export const geoPromptsRelations = relations(geoPrompts, ({ one }) => ({ + organization: one(organizations, { + fields: [geoPrompts.organizationId], + references: [organizations.id], + }), +})); + export const postCollectionsRelations = relations( postCollections, ({ one, many }) => ({ diff --git a/packages/ui/dither-kit.json b/packages/ui/dither-kit.json new file mode 100644 index 000000000..ff7a52320 --- /dev/null +++ b/packages/ui/dither-kit.json @@ -0,0 +1,196 @@ +{ + "$schema": "https://tripwire.sh/schema/dither-kit.json", + "lockfileVersion": 1, + "registry": "https://tripwire.sh", + "mode": "source", + "components": { + "area-chart": { + "version": "0.1.0", + "hash": "sha256:197d088204a38580fec861579c89e8c5aff7fe158222ce9b7a1a4b0a605e415e", + "files": [ + { + "path": "components/dither-kit/area-chart.tsx", + "hash": "sha256:307883b537e005386a8090499eaf3ea636767a10235271181cb089c3477eab1e" + }, + { + "path": "components/dither-kit/area.tsx", + "hash": "sha256:ecfb990f9083dd4c813eb431e3674822b7272076294de4ae038e81552d678532" + }, + { + "path": "components/dither-kit/cartesian-canvas.tsx", + "hash": "sha256:180a1525cbe10ec35bc79f689bd018cfdf115ebb4e1e6b1ff31d4bd37ecc6f09" + }, + { + "path": "components/dither-kit/sparkline.tsx", + "hash": "sha256:b5dab0b68af7ebc976ec51a74776761ac26d79f4aacd9c453a6e79c49e8d11ff" + } + ] + }, + "avatar": { + "version": "0.1.0", + "hash": "sha256:c7a639c023dcfb9f1365f11de35986d265de494b60c1887807aedf6504acf0cd", + "files": [ + { + "path": "components/dither-kit/avatar.tsx", + "hash": "sha256:bf9736b474a5e050b19b23fb4d858924758d6df1d0a1006eed95f1fc42b84cb5" + }, + { + "path": "components/dither-kit/lib.ts", + "hash": "sha256:41493601eb53a70f7629f6e50bad233f7185eaad42ba2397ca56ac1afcf1bb5f" + }, + { + "path": "components/dither-kit/palette.ts", + "hash": "sha256:fd020c230ad818a5d9d6dfdfcb6ef50b0dba012c77f44f1a61449fc9a65947f7" + }, + { + "path": "components/dither-kit/pixel.ts", + "hash": "sha256:eea1493d54a86d164b40d69c95956238777331ded7a556f099230dab161ab5a7" + } + ] + }, + "bar-chart": { + "version": "0.1.0", + "hash": "sha256:fb4a2e9bba84e3558e0e162de42eaf07a741557bfa43167c0e319cdfa75acd0d", + "files": [ + { + "path": "components/dither-kit/bar-canvas.tsx", + "hash": "sha256:80ed40b87eb3dbf3d9f768cf42b3b55f4f738db8b193c1a616f7abf525d83f87" + }, + { + "path": "components/dither-kit/bar-chart.tsx", + "hash": "sha256:e19ce3540f15756aba615eb19072aeec81d8ac71e51219691cc1d0be004c18bb" + }, + { + "path": "components/dither-kit/bar.tsx", + "hash": "sha256:2904ff1b0813ae20b976f5a78d57a314729ea3e890b18ba762bbb46d2f550057" + } + ] + }, + "core": { + "version": "0.1.0", + "hash": "sha256:c2d71baa1313a822cbef24de39414e21df7029fc80ad297ab8471d3d727235e9", + "files": [ + { + "path": "components/dither-kit/block-legend.tsx", + "hash": "sha256:4ddf215c6187ac0ca2640501c0342699bc210b33c1cec1d3e138ee3d38f77433" + }, + { + "path": "components/dither-kit/cartesian-root.tsx", + "hash": "sha256:6b5297d75de22ae5a0fc1960bef0419a4984b54ff493e7acd527aa0ccb150098" + }, + { + "path": "components/dither-kit/chart-context.tsx", + "hash": "sha256:955657f21fdbf7e04b80917be52b7c3839eb535b8d07f15053c06d9320683d8f" + }, + { + "path": "components/dither-kit/common-context.tsx", + "hash": "sha256:aafb26404977148144a3d81da81a678d5956e14024f394ccdc7ae881dd07e52a" + }, + { + "path": "components/dither-kit/dither-paint.ts", + "hash": "sha256:1bcd3d7de3ae3091caf63ed4c2c862d80d1899c9b55a70078acb12f7b44e51c7" + }, + { + "path": "components/dither-kit/dot.tsx", + "hash": "sha256:207bf90662237064940d14a4d8411135d75027b97a9582c0e61c8af74ea5d65a" + }, + { + "path": "components/dither-kit/grid.tsx", + "hash": "sha256:ad09d80b3898a4d3f588ff1f2f4f82984864c3918cb09d31350be6ac18d2446f" + }, + { + "path": "components/dither-kit/legend.tsx", + "hash": "sha256:2a067ebdc484c7bd9efb2afc4e8a0aa6718a14ec44ccd7bb94c871d192e501e6" + }, + { + "path": "components/dither-kit/lib.ts", + "hash": "sha256:41493601eb53a70f7629f6e50bad233f7185eaad42ba2397ca56ac1afcf1bb5f" + }, + { + "path": "components/dither-kit/palette.ts", + "hash": "sha256:fd020c230ad818a5d9d6dfdfcb6ef50b0dba012c77f44f1a61449fc9a65947f7" + }, + { + "path": "components/dither-kit/polar-context.tsx", + "hash": "sha256:514ac9a4984b036598a14ecd7acc29ee121e9259c8a85309d7ed3c1e22af6831" + }, + { + "path": "components/dither-kit/polar-root.tsx", + "hash": "sha256:63cecb6193ce3cd296b93b018e2441ccdefcc055cdc31bf131c808f0cf0c0c52" + }, + { + "path": "components/dither-kit/polar.ts", + "hash": "sha256:3ad3f99bac2ee21e6580abd6903720af11cedcb8d8c69a06c40690be6229672c" + }, + { + "path": "components/dither-kit/reference-line.tsx", + "hash": "sha256:b0df8cf1144ab56fcce577bac4a39e06d9215a1fdf81bb112a4190a0445b138b" + }, + { + "path": "components/dither-kit/scales.ts", + "hash": "sha256:62ec74acdd3e335b959423a228b47edd70c14f07a468a385d039f5c6b66ebc2b" + }, + { + "path": "components/dither-kit/series-context.tsx", + "hash": "sha256:1802f1f488cc46542294f533c55fae03a4b067feca0761df75ce21de8a6a429a" + }, + { + "path": "components/dither-kit/tooltip.tsx", + "hash": "sha256:d053ac2135c6f29d011cb0a83be29a797f8954ccbb02916402d2d3c2a52b17f2" + }, + { + "path": "components/dither-kit/use-chart-dimensions.ts", + "hash": "sha256:74b7f2a08cce5b891348eed845fc8c218ed16fce362ef4f35587ec4cc864c834" + }, + { + "path": "components/dither-kit/x-axis.tsx", + "hash": "sha256:7997bd96c341816a0f1cc6f966ad386fedc78be39e51201fa368dab1fd943d62" + }, + { + "path": "components/dither-kit/y-axis.tsx", + "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/package.json b/packages/ui/package.json index 64032957a..85cf0cb01 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -11,6 +11,7 @@ "./postcss.config": "./postcss.config.mjs", "./lib/*": "./src/lib/*.ts", "./components/*": "./src/components/*.tsx", + "./components/dither-kit/palette": "./src/components/dither-kit/palette.ts", "./components/shared/*": "./src/components/shared/*.tsx", "./components/kibo-ui/*": "./src/components/kibo-ui/*/index.tsx", "./hooks/*": "./src/hooks/*.ts", @@ -27,9 +28,12 @@ "@xyflow/react": "^12.10.0", "ai": "6.0.206", "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cmdk": "^1.1.1", "cnfast": "^0.0.6", "color": "^5.0.3", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.562.0", @@ -49,6 +53,7 @@ "shiki": "^3.21.0", "sonner": "^2.0.7", "streamdown": "^2.0.1", + "tailwind-merge": "^3.6.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.1" @@ -56,6 +61,8 @@ "devDependencies": { "@notra/typescript-config": "*", "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.8", "@types/node": "^22.15.3", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", diff --git a/packages/ui/src/components/dither-kit/area-chart.tsx b/packages/ui/src/components/dither-kit/area-chart.tsx new file mode 100644 index 000000000..a8c622091 --- /dev/null +++ b/packages/ui/src/components/dither-kit/area-chart.tsx @@ -0,0 +1,25 @@ +"use client" + +import { CartesianCanvas } from "./cartesian-canvas" +import { type CartesianChartProps, CartesianRoot } from "./cartesian-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 + +/** Composable dither **area** chart. Compose ``, ``, axes, … inside. */ +export function AreaChart( + props: CartesianChartProps +) { + return +} + +/** Composable dither **line** chart — `` series with a glow under the line. */ +export function LineChart( + props: CartesianChartProps +) { + return +} + +export type AreaChartProps = CartesianChartProps diff --git a/packages/ui/src/components/dither-kit/area.tsx b/packages/ui/src/components/dither-kit/area.tsx new file mode 100644 index 000000000..e48fdfa27 --- /dev/null +++ b/packages/ui/src/components/dither-kit/area.tsx @@ -0,0 +1,102 @@ +"use client" + +import { type ReactNode, useEffect } from "react" +import { + type AreaVariant, + type SeriesKind, + type StrokeVariant, + useChartPart, +} from "./chart-context" +import { SeriesContext } from "./series-context" + +export type SeriesProps = { + dataKey: string + variant?: AreaVariant + strokeVariant?: StrokeVariant + isClickable?: boolean + children?: ReactNode +} + +/** + * Shared implementation for the continuous series (``, ``). The + * dithered fill/line is painted on the canvas; this registers the series so the + * canvas knows how to draw it, wires click-to-select via a transparent band + * polygon, and exposes the series to child ``/`` markers. + */ +function CartesianSeries({ + part, + kind, + dataKey, + variant = "gradient", + strokeVariant = "solid", + isClickable = false, + children, +}: SeriesProps & { part: string; kind: SeriesKind }) { + const ctx = useChartPart(part, kind === "line" ? "line" : "area") + const { registerSeries, unregisterSeries } = ctx + + if (process.env.NODE_ENV !== "production" && !ctx.config[dataKey]) { + console.warn( + `<${part} dataKey="${dataKey}" />: "${dataKey}" is not in the chart \`config\`. Add it so the series has a colour and label.` + ) + } + + useEffect(() => { + registerSeries({ dataKey, kind, variant, strokeVariant }) + return () => unregisterSeries(dataKey) + }, [dataKey, kind, variant, strokeVariant, registerSeries, unregisterSeries]) + + const band = ctx.bands[dataKey] + if (!ctx.ready || !band) return null + + const seed = ctx.seedOf(dataKey) + const emphasis = ctx.selectedDataKey ?? ctx.focusDataKey + const dimmed = emphasis !== null && emphasis !== dataKey + const onClick = isClickable + ? () => ctx.selectDataKey(ctx.selectedDataKey === dataKey ? null : dataKey) + : undefined + + // Transparent hit polygon tracing the series' own band, so clicking a series + // selects *that* series. The Legend offers the same toggle accessibly. + // One pass out along the top edge, one pass back along the floor. + let hitPath: string | null = null + if (isClickable) { + const parts: string[] = [] + band.forEach((b, i) => { + parts.push(`${i === 0 ? "M" : "L"}${ctx.xCenter(i)},${ctx.y(b[1])}`) + }) + for (let i = band.length - 1; i >= 0; i -= 1) { + parts.push(`L${ctx.xCenter(i)},${ctx.y(band[i]?.[0] ?? 0)}`) + } + hitPath = `${parts.join(" ")} Z` + } + + return ( + <> + {hitPath && ( + // biome-ignore lint/a11y/noStaticElementInteractions: progressive enhancement; the Legend offers the same toggle accessibly + + )} + + {children} + + + ) +} + +export type AreaProps = SeriesProps + +/** One area series — dithered fill from the value line down to its floor. */ +export function Area(props: AreaProps) { + return +} + +/** One line series — bright line with a thin dither glow hugging it. */ +export function Line(props: AreaProps) { + return +} diff --git a/packages/ui/src/components/dither-kit/avatar.tsx b/packages/ui/src/components/dither-kit/avatar.tsx new file mode 100644 index 000000000..a1f03785a --- /dev/null +++ b/packages/ui/src/components/dither-kit/avatar.tsx @@ -0,0 +1,212 @@ +"use client" + +import { useEffect, useRef } from "react" +import { cn } from "./lib" +import { rgb } from "./palette" +import { + BAYER4, + clamp01, + fnv1a, + hueFill, + type PixelBloom, + pixelBloomStyle, + pixelPrefersReducedMotion, + xorshift32, +} from "./pixel" + +// 8×8 cells, mirrored across one axis → 32 free pattern bits. With the mirror +// axis bit and 180 hues that's 2^33 × 180 ≈ 1.5 trillion distinct avatars. +const GRID = 8 +const CELL_PX = 4 // backing px per cell → a 32×32 canvas, scaled up pixelated + +export type AvatarMirror = "auto" | "horizontal" | "vertical" + +export type DitherAvatarProps = { + /** The seed — same name, same avatar, every time. */ + name: string + /** Hue override (0–360). Derived from the name when omitted. */ + hue?: number + /** Mirror axis. "auto" picks one from the name — half the avatars fold + * left/right, half fold top/bottom. */ + mirror?: AvatarMirror + /** Square size in px. Omit to size via className (e.g. `size-12`). */ + size?: number + /** Glow on the dither fill. */ + bloom?: PixelBloom + /** Play the Bayer-ordered materialize entrance. */ + animate?: boolean + animationDuration?: number + /** Bump to replay the entrance. */ + replayToken?: number + className?: string +} + +type AvatarModel = { + on: boolean[] // GRID×GRID, row-major + density: number[] // per-cell dither density for on cells + fill: [number, number, number] +} + +/** + * Derive the full 8×8 cell grid from the name: 32 pattern bits + the mirror + * axis + the hue + per-cell densities, all from one deterministic PRNG stream. + * Every draw happens unconditionally so overriding `hue` or `mirror` never + * shifts the pattern. + */ +function avatarModel( + name: string, + hueProp: number | undefined, + mirrorProp: AvatarMirror +): AvatarModel { + const rand = xorshift32(fnv1a(name)) + const bits = Array.from({ length: 32 }, () => rand() < 0.5) + const drawnVertical = rand() < 0.5 + const drawnHue = Math.floor(rand() * 180) * 2 + const halfDensity = Array.from({ length: 32 }, () => 0.55 + rand() * 0.45) + + const vertical = + mirrorProp === "auto" ? drawnVertical : mirrorProp === "vertical" + const hue = hueProp ?? drawnHue + + const on = new Array(GRID * GRID) + const density = new Array(GRID * GRID) + for (let r = 0; r < GRID; r++) { + for (let c = 0; c < GRID; c++) { + // Fold across the chosen axis: left/right symmetric ("horizontal" + // mirror) or top/bottom symmetric ("vertical"). + const i = vertical + ? Math.min(r, GRID - 1 - r) * GRID + c + : r * (GRID / 2) + Math.min(c, GRID - 1 - c) + on[r * GRID + c] = bits[i] ?? false + density[r * GRID + c] = halfDensity[i] ?? 0 + } + } + return { on, density, fill: hueFill(hue) } +} + +/** + * Paint the avatar, optionally sweeping cells in with the Bayer-ordered + * materialize entrance. Lives outside the component (same shape as the chart + * canvases). Returns a cleanup that cancels the entrance loop. + */ +function paintAvatar( + canvas: HTMLCanvasElement, + bloomCanvas: HTMLCanvasElement | null, + model: AvatarModel, + animate: boolean, + duration: number +): (() => void) | undefined { + const ctx = canvas.getContext("2d") + if (!ctx) return undefined + const px = GRID * CELL_PX + canvas.width = px + canvas.height = px + const bloomCtx = bloomCanvas?.getContext("2d") ?? null + if (bloomCanvas) { + bloomCanvas.width = px + bloomCanvas.height = px + } + + const draw = (progress: number) => { + ctx.clearRect(0, 0, px, px) + for (let r = 0; r < GRID; r++) { + for (let c = 0; c < GRID; c++) { + if (!model.on[r * GRID + c]) continue + // Cells materialize in Bayer order — the entrance is made of the same + // matrix as the texture. + const start = (BAYER4[r % 4]?.[c % 4] ?? 0) * 0.7 + const cellAlpha = clamp01((progress - start) / 0.3) + if (cellAlpha <= 0) continue + const density = model.density[r * GRID + c] ?? 0 + const base = 0.35 + 0.65 * density + for (let py = 0; py < CELL_PX; py++) { + for (let pxi = 0; pxi < CELL_PX; pxi++) { + const gx = c * CELL_PX + pxi + const gy = r * CELL_PX + py + const lit = density > (BAYER4[gy & 3]?.[gx & 3] ?? 0) + // On/off cells modulate alpha tiers of the one fill colour, so the + // avatar holds up on light and dark backgrounds alike. + const alpha = (lit ? base : base * 0.35) * cellAlpha + ctx.fillStyle = rgb(model.fill, 1, alpha) + ctx.fillRect(gx, gy, 1, 1) + } + } + } + } + if (bloomCtx) { + bloomCtx.clearRect(0, 0, px, px) + bloomCtx.drawImage(canvas, 0, 0) + } + } + + if (!animate || pixelPrefersReducedMotion()) { + draw(1) + return undefined + } + + let raf = 0 + const startTime = performance.now() + const tick = (now: number) => { + const t = clamp01((now - startTime) / duration) + draw(1 - (1 - t) ** 3) + if (t < 1) raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + return () => cancelAnimationFrame(raf) +} + +/** + * Generative dithered avatar — a mirrored 8×8 pixel glyph derived from a name, + * rendered with the ordered-dither texture the charts are made of. Same name, + * same avatar; ~1.5 trillion combinations across pattern, mirror axis, and hue. + */ +export function DitherAvatar({ + name, + hue, + mirror = "auto", + size, + bloom = "off", + animate = true, + animationDuration = 600, + replayToken = 0, + className, +}: DitherAvatarProps) { + const canvasRef = useRef(null) + const bloomRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + return paintAvatar( + canvas, + bloomRef.current, + avatarModel(name, hue, mirror), + animate, + animationDuration + ) + }, [name, hue, mirror, animate, animationDuration, replayToken, bloom]) + + const bloomStyle = pixelBloomStyle(bloom) + + return ( +
+ + {bloomStyle && ( + + )} +
+ ) +} diff --git a/packages/ui/src/components/dither-kit/bar-canvas.tsx b/packages/ui/src/components/dither-kit/bar-canvas.tsx new file mode 100644 index 000000000..145175c77 --- /dev/null +++ b/packages/ui/src/components/dither-kit/bar-canvas.tsx @@ -0,0 +1,227 @@ +"use client" + +import { useEffect, useMemo, useRef } from "react" +import { useChart } from "./chart-context" +import { + backingSize, + bloomLayerStyle, + clamp01, + easeOutCubic, + paintColumn, + prefersReducedMotion, +} from "./dither-paint" + +type Bars = { top: number[]; base: number[] } // per data index, in backing rows + +// Fraction of the timeline spent staggering bar starts — the rest is each bar's +// own grow window, so the rise sweeps across the chart as a wave. +const STAGGER = 0.55 + +/** + * Dither canvas for bar charts. Each category owns a band; grouped series split + * it into side-by-side bars, stacked series share its full width and pile in y. + * Every bar is filled with the shared {@link paintColumn} ordered dither. Bars + * grow up from their base in a staggered left-to-right wave (eased), and the + * hovered category lifts while the rest dim. + */ +export function BarCanvas() { + const ctx = useChart() + const canvasRef = useRef(null) + const bloomRef = useRef(null) + + const { width, height } = ctx.plot + const { cols, rows } = backingSize(width, height) + const { ready, configKeys, bands, y } = ctx + + // Memoized: per-series bar tops/bases (backing rows) over the data indices. + // The canvas re-renders on every hover/cursor tick, so pin this map to the + // exact ctx fields it reads plus the backing geometry — a bar hover must not + // rebuild every band's geometry. + const targets = useMemo(() => { + const out: Record = {} + if (!ready) return out + const h = height || 1 + for (const key of configKeys) { + const band = bands[key] + if (!band) continue + out[key] = { + top: band.map((b) => (y(b[1]) / h) * (rows - 1)), + base: band.map((b) => (y(b[0]) / h) * (rows - 1)), + } + } + return out + }, [ready, configKeys, bands, y, height, rows]) + + // The RAF loop reads these through refs so it always sees the latest values; + // refs are written in an effect (never during render) — mutating a ref + // mid-render tears under Strict Mode / concurrent rendering. + const state = useRef(ctx) + const targetsRef = useRef(targets) + useEffect(() => { + state.current = ctx + targetsRef.current = targets + }) + + 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 + const fx = cols / Math.max(width, 1) + + // Eased grow factor for bar `i` at global progress `prog`. + const barProgress = (i: number, len: number, prog: number) => { + if (!animate) return 1 + const start = len > 1 ? (i / (len - 1)) * STAGGER : 0 + return easeOutCubic(clamp01((prog - start) / (1 - STAGGER))) + } + + const paint = (prog: number) => { + const s = state.current + c.clearRect(0, 0, cols, rows) + const stacked = s.stackType === "stacked" || s.stackType === "percent" + const keys = s.configKeys + keys.forEach((key, si) => { + const t = targetsRef.current[key] + if (!t) return + const seed = s.seedOf(key) + const variant = s.seriesSpecs[key]?.variant ?? "gradient" + const emphasis = s.selectedDataKey ?? s.focusDataKey + const selDim = emphasis !== null && emphasis !== key ? 0.3 : 1 + for (let i = 0; i < s.dataLength; i++) { + const bp = barProgress(i, s.dataLength, prog) + const base = t.base[i] ?? rows - 1 + const grown = base + ((t.top[i] ?? base) - base) * bp + // Bars grow from the zero baseline toward the value. Positive values + // sit above the baseline (smaller pixel), negative ones below it — + // paintColumn wants the higher edge first, so order the pair. + const top = Math.min(grown, base) + const bottom = Math.max(grown, base) + const active = s.hoverIndex === i + const hoverDim = + s.hoverIndex != null && !active && s.isMouseInChart ? 0.5 : 1 + const slot = s.barSlot(i, si, keys.length) + const c0 = Math.round(slot.x * fx) + const c1 = Math.round((slot.x + slot.width) * fx) + for (let x = c0; x < c1; x++) { + paintColumn(c, x, top, bottom, seed, { + variant, + intensity: intensity + (active ? 0.4 : 0), + dim: selDim * hoverDim, + stacked, + }) + } + } + }) + } + + 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 draw = (now: number) => { + raf = requestAnimationFrame(draw) + const s = state.current + if (!s.ready) return + if (bloomCtx) { + const on = + s.bloom !== "off" && + (!s.bloomOnHover || s.isMouseInChart || s.hovered) + if (on) { + bloomCtx.clearRect(0, 0, cols, rows) + bloomCtx.drawImage(canvas, 0, 0) + } + } + if (s.revision !== lastRevision) { + lastRevision = s.revision + animStart = 0 // re-play the wave on data change / replay + lastProg = -1 + } + if (!animStart) animStart = now + const prog = animate ? Math.min(1, (now - animStart) / duration) : 1 + + if (prog !== lastProg) { + lastProg = prog + needsFill = true + } + 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 || s.hovered ? 1 : 0 + if (Math.abs(intensity - itTarget) > 0.001) { + intensity += (itTarget - intensity) * (reduce ? 1 : 0.16) + needsFill = true + } else intensity = itTarget + + // Live tweak repaint (variant, stacking) without replaying the wave. + const paintSig = `${s.stackType}|${s.configKeys + .map((k) => s.seriesSpecs[k]?.variant ?? "") + .join(",")}` + if (paintSig !== lastPaintSig) { + lastPaintSig = paintSig + needsFill = true + } + + if (!needsFill) return + paint(prog) + needsFill = false + } + + raf = requestAnimationFrame(draw) + return () => cancelAnimationFrame(raf) + }, [cols, rows, width]) + + const bloomActive = ctx.bloomOnHover + ? ctx.isMouseInChart || ctx.hovered + : true + const bloom = bloomLayerStyle(ctx.bloom, bloomActive) + const pos = { + left: ctx.margins.left, + top: ctx.margins.top, + width, + height, + } as const + + return ( + <> + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/bar-chart.tsx b/packages/ui/src/components/dither-kit/bar-chart.tsx new file mode 100644 index 000000000..423e3f580 --- /dev/null +++ b/packages/ui/src/components/dither-kit/bar-chart.tsx @@ -0,0 +1,14 @@ +"use client" + +import { BarCanvas } from "./bar-canvas" +import { type CartesianChartProps, CartesianRoot } from "./cartesian-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 + +/** Composable dither **bar** chart — `` series, grouped or stacked. */ +export function BarChart(props: CartesianChartProps) { + return +} diff --git a/packages/ui/src/components/dither-kit/bar.tsx b/packages/ui/src/components/dither-kit/bar.tsx new file mode 100644 index 000000000..15c8217d6 --- /dev/null +++ b/packages/ui/src/components/dither-kit/bar.tsx @@ -0,0 +1,83 @@ +"use client" + +import { type ReactNode, useEffect } from "react" +import { + type AreaVariant, + type StrokeVariant, + useChartPart, +} from "./chart-context" +import { SeriesContext } from "./series-context" + +export type BarProps = { + dataKey: string + variant?: AreaVariant + strokeVariant?: StrokeVariant + isClickable?: boolean + children?: ReactNode +} + +/** + * One bar series. The dithered bars are painted on the canvas; this registers + * the series and (when `isClickable`) lays transparent hit rects over each bar + * — using the shared `barSlot` geometry so clicks line up with the pixels — to + * select the series. The Legend offers the same toggle accessibly. + */ +export function Bar({ + dataKey, + variant = "gradient", + strokeVariant = "solid", + isClickable = false, + children, +}: BarProps) { + const ctx = useChartPart("Bar", "bar") + const { registerSeries, unregisterSeries } = 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(() => { + registerSeries({ dataKey, kind: "bar", variant, strokeVariant }) + return () => unregisterSeries(dataKey) + }, [dataKey, variant, strokeVariant, registerSeries, unregisterSeries]) + + const band = ctx.bands[dataKey] + if (!ctx.ready || !band) return null + + const seed = ctx.seedOf(dataKey) + const dimmed = ctx.selectedDataKey !== null && ctx.selectedDataKey !== dataKey + const si = ctx.configKeys.indexOf(dataKey) + const n = ctx.configKeys.length + const onClick = () => + ctx.selectDataKey(ctx.selectedDataKey === dataKey ? null : dataKey) + + return ( + <> + {isClickable && + band.map((b, i) => { + const slot = ctx.barSlot(i, si, n) + const top = ctx.y(b[1]) + const base = ctx.y(b[0]) + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: progressive enhancement; the Legend offers the same toggle accessibly + + ) + })} + + {children} + + + ) +} diff --git a/packages/ui/src/components/dither-kit/block-legend.tsx b/packages/ui/src/components/dither-kit/block-legend.tsx new file mode 100644 index 000000000..54d14c20d --- /dev/null +++ b/packages/ui/src/components/dither-kit/block-legend.tsx @@ -0,0 +1,63 @@ +"use client" + +import type { ChartConfig } from "./chart-context" +import { cn } from "./lib" +import { rgb, seedOfColor } from "./palette" + +/** + * An in-flow legend rendered as a sibling of the chart rather than an overlay. + * + * The overlay {@link Legend} is pinned absolutely to the top of the plot, so + * with more than ~3 entries (or a narrow container) its wrapped rows sit on top + * of the chart. `` lives in normal document flow, so it can never + * overlap the plot at any width — use it for multi-entry charts (donuts, many + * series) and reserve the overlay `` for ≤2–3 entries. + * + * It needs no chart context: feed it the same `config` you pass the chart, and + * optionally a `values` map to show a number beside each entry (e.g. allocation + * shares or totals). + */ +export function BlockLegend({ + config, + values, + valueFormatter = (v) => String(v), + align = "start", + className, +}: { + config: ChartConfig + values?: Record + valueFormatter?: (value: number) => string + align?: "start" | "center" | "end" + className?: string +}) { + return ( +
    + {Object.entries(config).map(([name, entry]) => { + const seed = seedOfColor(entry.color) + const value = values?.[name] + return ( +
  • + + {entry.label ?? name} + {value !== undefined ? ( + {valueFormatter(value)} + ) : null} +
  • + ) + })} +
+ ) +} diff --git a/packages/ui/src/components/dither-kit/cartesian-canvas.tsx b/packages/ui/src/components/dither-kit/cartesian-canvas.tsx new file mode 100644 index 000000000..55bb2006a --- /dev/null +++ b/packages/ui/src/components/dither-kit/cartesian-canvas.tsx @@ -0,0 +1,405 @@ +"use client" + +import { type RefObject, useEffect, useMemo, useRef } from "react" +import { type ChartContextValue, useChart } from "./chart-context" +import { + backingSize, + bloomLayerStyle, + easeInOutCubic, + paintColumn, + prefersReducedMotion, + resample, +} from "./dither-paint" +import { rgb } from "./palette" + +type Star = { key: string; xi: number; depth: number; phase: number } +type Surface = { top: number[]; floor: number[] } + +type LoopArgs = { + canvas: HTMLCanvasElement + bloomCanvas: HTMLCanvasElement | null + cols: number + rows: number + state: RefObject + targets: RefObject> + stars: RefObject +} + +/** + * The requestAnimationFrame paint loop — eases each series toward its target + * surface, paints the dither fill (with the entrance reveal), then layers the + * crosshair marker and winking stars on top. Lives outside the component so the + * component stays small and this hot closure isn't re-created on every render. + * Returns a cleanup that cancels the loop. + */ +function startCartesianLoop({ + canvas, + bloomCanvas, + cols, + rows, + state, + targets, + stars, +}: LoopArgs): (() => void) | undefined { + const c = canvas.getContext("2d") + if (!c || cols <= 0 || rows <= 0) return undefined + canvas.width = cols + canvas.height = rows + + const off = document.createElement("canvas") + off.width = cols + off.height = rows + const octx = off.getContext("2d") + if (!octx) return undefined + + // Bloom layer: a blurred, additive copy of the crisp canvas. + const bloomCtx = bloomCanvas?.getContext("2d") ?? null + if (bloomCanvas) { + bloomCanvas.width = cols + bloomCanvas.height = rows + } + + const reduce = prefersReducedMotion() + const EASE = reduce ? 1 : 0.18 + const animate = state.current.animate && !reduce + const duration = state.current.animationDuration + const current: Record = {} + + // `reveal` (0–1) sweeps the fill in left-to-right on first paint. + const paintFill = (intensity: number, reveal: number) => { + octx.clearRect(0, 0, cols, rows) + const s = state.current + const stacked = s.stackType === "stacked" || s.stackType === "percent" + const revealCols = Math.ceil(reveal * cols) + s.configKeys.forEach((key, si) => { + const cur = current[key] + if (!cur) return + const seed = s.seedOf(key) + const variant = s.seriesSpecs[key]?.variant ?? "gradient" + const isLine = + (s.seriesSpecs[key]?.kind ?? + (s.chartType === "line" ? "line" : "area")) === "line" + const emphasis = s.selectedDataKey ?? s.focusDataKey + const dim = emphasis !== null && emphasis !== key ? 0.3 : 1 + // Overlapping (non-stacked) layers thin out front-to-back so they + // read as distinct layers instead of a muddy blend. + const sparse = stacked ? 0 : si * 0.14 + for (let x = 0; x < cols; x++) { + if (x > revealCols) break + // For a value that dips below the zero baseline the value line ends up + // *below* the floor in pixels; paintColumn needs the higher edge first, + // so order the pair (a no-op for the common positive case). + const a = cur.top[x] ?? 0 + const b = cur.floor[x] ?? 0 + paintColumn(octx, x, Math.min(a, b), Math.max(a, b), seed, { + variant, + intensity, + dim, + stacked: stacked && !isLine, + sparse, + }) + } + }) + } + + let raf = 0 + let tick = 0 + let last = 0 + let animStart = 0 + let lastProg = -1 + let lastRevision = state.current.revision + let entranceReported = !animate + let intensity = 0 + let needsFill = true + let lastPaintSig = "" + let lastSelected: string | null | undefined = Symbol() as never + + const draw = (now: number) => { + raf = requestAnimationFrame(draw) + const s = state.current + if (!s.ready) return + // Keep the bloom layer in sync with the crisp canvas while it's active. + if (bloomCtx) { + const on = + s.bloom !== "off" && (!s.bloomOnHover || s.isMouseInChart || s.hovered) + if (on) { + bloomCtx.clearRect(0, 0, cols, rows) + bloomCtx.drawImage(canvas, 0, 0) + } + } + const tgt = targets.current + if (s.revision !== lastRevision) { + lastRevision = s.revision + animStart = 0 // re-play the entrance on data change / replay + lastProg = -1 + entranceReported = false + } + if (!animStart) animStart = now + const prog = animate ? Math.min(1, (now - animStart) / duration) : 1 + const progChanged = prog !== lastProg + // Tell the context the reveal is done so DOM markers fade in in sync. + if (prog >= 1 && !entranceReported) { + entranceReported = true + s.markEntranceDone() + } + + let moving = false + for (const key of s.configKeys) { + const t = tgt[key] + if (!t) continue + const cur = current[key] + if (!cur || cur.top.length !== cols) { + current[key] = { top: t.top.slice(), floor: t.floor.slice() } + needsFill = true + continue + } + for (let x = 0; x < cols; x++) { + const tTop = t.top[x] ?? 0 + const tFloor = t.floor[x] ?? 0 + const dt = tTop - (cur.top[x] ?? 0) + const df = tFloor - (cur.floor[x] ?? 0) + if (Math.abs(dt) > 0.01 || Math.abs(df) > 0.01) { + cur.top[x] = (cur.top[x] ?? 0) + dt * EASE + cur.floor[x] = (cur.floor[x] ?? 0) + df * EASE + moving = true + } else { + cur.top[x] = tTop + cur.floor[x] = tFloor + } + } + } + for (const key of Object.keys(current)) { + if (!tgt[key]) { + delete current[key] + needsFill = true + } + } + if (moving) needsFill = true + const emphasisNow = s.selectedDataKey ?? s.focusDataKey + if (emphasisNow !== lastSelected) { + lastSelected = emphasisNow + needsFill = true + } + + const itTarget = s.isMouseInChart || s.hovered ? 1 : 0 + let settling = false + if (Math.abs(intensity - itTarget) > 0.001) { + intensity += (itTarget - intensity) * 0.16 + settling = true + needsFill = true + } else intensity = itTarget + + // Live hover wins; the controlled markerIndex (e.g. a committed point) + // is the fallback shown when nothing is hovered. + const marker = s.hoverIndex != null ? s.hoverIndex : s.markerIndex + const winkDue = !reduce && now - last >= 100 + // Repaint when a tweak-driven paint input changes (variant, stacking) so + // the panel updates the fill live — without resetting the entrance reveal. + const paintSig = `${s.stackType}|${s.configKeys + .map((k) => s.seriesSpecs[k]?.variant ?? "") + .join(",")}` + const sigChanged = paintSig !== lastPaintSig + if (sigChanged) { + lastPaintSig = paintSig + needsFill = true + } + if ( + !( + moving || + settling || + winkDue || + marker != null || + progChanged || + sigChanged + ) + ) + return + if (progChanged) { + lastProg = prog + needsFill = true + } + if (winkDue) { + last = now + tick += 1 + } + + // Reveal front (left-to-right) — stars + crosshair stay behind it so + // they don't float over the not-yet-drawn area during the entrance. + const reveal = animate ? easeInOutCubic(prog) : 1 + const revealCols = reveal * cols + + if (needsFill) { + paintFill(intensity, reveal) + needsFill = false + } + c.clearRect(0, 0, cols, rows) + c.drawImage(off, 0, 0) + + const mx = + marker != null && s.dataLength > 1 + ? Math.round((marker / (s.dataLength - 1)) * (cols - 1)) + : -1 + if (mx >= 0 && mx <= revealCols) { + for (const key of s.configKeys) { + const cur = current[key] + if (!cur) continue + const seed = s.seedOf(key) + const my = Math.round(cur.top[mx] ?? 0) + // Full-height column + a chunky marker block at the point — the + // series colour at higher opacity, so it reads on either theme. + c.fillStyle = rgb(seed.fill, 1, 0.55) + for (let y = my; y < rows; y++) c.fillRect(mx, y, 1, 1) + c.fillStyle = rgb(seed.fill) + c.fillRect(mx - 1, my - 1, 3, 3) + } + } + + for (const star of stars.current) { + const cur = current[star.key] + if (!cur) continue + const sx = Math.round( + (star.xi / Math.max(s.dataLength - 1, 1)) * (cols - 1) + ) + if (sx > revealCols) continue // behind the reveal front + const top = cur.top[sx] ?? 0 + const floor = cur.floor[sx] ?? rows - 1 + const sy = Math.round(top + star.depth * (floor - top)) + const tw = reduce ? 0.85 : (Math.sin((tick + star.phase) * 0.35) + 1) / 2 + const lift = tw * (0.7 + 0.3 * intensity) + if (lift < 0.55 || sy < 0 || sy >= rows) continue + // Sparkles glint in the series colour via opacity (the `lift` wink) + // rather than a lighter shade — so they never read as stray white + // pixels on a light background. + const starColor = s.seedOf(star.key).fill + c.fillStyle = rgb(starColor, 1, lift) + c.fillRect(sx, sy, 1, 1) + // At the peak of a wink the star flares into a 4-point glint. + if (tw > 0.9) { + c.fillStyle = rgb(starColor, 1, lift * 0.6 * (tw - 0.9) * 10) + c.fillRect(sx - 1, sy, 1, 1) + c.fillRect(sx + 1, sy, 1, 1) + c.fillRect(sx, sy - 1, 1, 1) + c.fillRect(sx, sy + 1, 1, 1) + } + } + } + + raf = requestAnimationFrame(draw) + return () => cancelAnimationFrame(raf) +} + +/** + * Continuous dither canvas for area and line charts. Each series is reduced to a + * `[top, floor]` band per backing column: areas fill from their value line down + * to their floor; lines fill only a thin glow band hugging the line. The shared + * {@link paintColumn} renders the ordered-dither scatter, capped by the bright + * series line, with winking stars + scrub crosshair on top. + */ +export function CartesianCanvas() { + const ctx = useChart() + const canvasRef = useRef(null) + const bloomRef = useRef(null) + + const { width, height } = ctx.plot + const { cols, rows } = backingSize(width, height) + const { ready, chartType, configKeys, bands, seriesSpecs, y, dataLength } = ctx + + // Memoized: the pricey bit in the render path — a `resample` per series to + // the backing column count. The canvas re-renders on every hover/cursor tick + // (it consumes ctx), so without this the whole surface is rebuilt each time. + // Pinned to the exact ctx fields it reads, plus the backing geometry. + const targets = useMemo(() => { + const out: Record = {} + if (!ready) return out + const h = height || 1 + const glow = Math.max(6, Math.round(rows * 0.16)) + const defaultKind = chartType === "line" ? "line" : "area" + for (const key of configKeys) { + const band = bands[key] + if (!band) continue + const line = (seriesSpecs[key]?.kind ?? defaultKind) === "line" + const top = band.map((b) => (y(b[1]) / h) * (rows - 1)) + const floor = band.map((b, i) => + line ? Math.min(rows - 1, (top[i] ?? 0) + glow) : (y(b[0]) / h) * (rows - 1) + ) + out[key] = { top: resample(top, cols), floor: resample(floor, cols) } + } + return out + }, [ready, chartType, configKeys, bands, seriesSpecs, y, height, rows, cols]) + + // Memoized: the star field is deterministic — only its shape (series × + // column count) matters, so it need not be rebuilt on unrelated re-renders. + const stars = useMemo(() => { + const out: Star[] = [] + const per = Math.max(4, Math.round(cols / 14)) + configKeys.forEach((key, k) => { + for (let i = 0; i < per; i++) { + const seed = i * 67 + 13 + k * 131 + out.push({ + key, + xi: seed % Math.max(dataLength, 1), + depth: ((seed * 53 + 7) % 100) / 100, + phase: (seed * 41) % 360, + }) + } + }) + return out + }, [configKeys, dataLength, cols]) + + // The RAF loop reads these through refs so it always sees the latest values + // without re-subscribing. Refs are written in an effect (never during + // render) — mutating a ref mid-render is a React anti-pattern that tears + // under Strict Mode / concurrent rendering. + const stateRef = useRef(ctx) + const targetsRef = useRef(targets) + const starsRef = useRef(stars) + useEffect(() => { + stateRef.current = ctx + targetsRef.current = targets + starsRef.current = stars + }) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + return startCartesianLoop({ + canvas, + bloomCanvas: bloomRef.current, + cols, + rows, + state: stateRef, + targets: targetsRef, + stars: starsRef, + }) + }, [cols, rows]) + + const bloomActive = ctx.bloomOnHover + ? ctx.isMouseInChart || ctx.hovered + : true + const bloom = bloomLayerStyle(ctx.bloom, bloomActive) + const pos = { + left: ctx.margins.left, + top: ctx.margins.top, + width, + height, + } as const + + return ( + <> + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/cartesian-root.tsx b/packages/ui/src/components/dither-kit/cartesian-root.tsx new file mode 100644 index 000000000..590030a63 --- /dev/null +++ b/packages/ui/src/components/dither-kit/cartesian-root.tsx @@ -0,0 +1,192 @@ +"use client" + +import { + Children, + type ComponentType, + isValidElement, + type ReactNode, +} from "react" +import { + type ChartConfig, + ChartContext, + type ChartType, + type Margins, + useChartController, +} from "./chart-context" +import { CommonChartContext } from "./common-context" +import type { BloomInput } from "./dither-paint" +import { cn } from "./lib" +import type { StackType } from "./scales" +import { useChartDimensions } from "./use-chart-dimensions" + +// `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 + +const DEFAULT_MARGINS: Margins = { + top: 10, + right: 12, + bottom: 22, + left: 36, +} + +export type CartesianChartProps = { + data: TData[] + config: ChartConfig + children: ReactNode + stackType?: StackType + margins?: Partial + className?: string + animate?: boolean + animationDuration?: number + replayToken?: number // change to re-play the entrance without remounting + /** Set false for a decorative sparkline: keeps the hover lift but no scrub + * crosshair / tooltip. */ + interactive?: boolean + /** Controlled crosshair position (e.g. a committed point) — overrides the + * internal hover when set. */ + markerIndex?: number | null + /** Parent-driven hover (e.g. the whole card/row) — lifts the fill. */ + hovered?: boolean + /** Glow on the dither fill. */ + bloom?: BloomInput + /** Only bloom while the chart is hovered. */ + bloomOnHover?: boolean + /** Fires with the scrubbed index as the pointer moves (null on leave). */ + onHoverChange?: (index: number | null) => void + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +} + +/** Which render layer a composed part targets — defaults to the front SVG. */ +function layerOf(node: ReactNode): "back" | "dom" | "svg" { + if (!isValidElement(node) || typeof node.type === "string") return "svg" + return (node.type as { chartLayer?: "back" | "dom" }).chartLayer ?? "svg" +} + +/** + * Shared root for the cartesian dither charts (area, line, bar). Owns the + * measured size, the shared context, and pointer interaction; every visual is + * composed as children. Back chrome (grid) sits behind the dither canvas; the + * canvas paints the fill/line/bars + stars; front chrome (axes, dots) and DOM + * legend/tooltip layer on top. `chartType` drives the scales/interaction and the + * `Canvas` prop supplies the family's painter (continuous for area/line, bars for + * bar) — so each chart ships only its own canvas. + */ +export function CartesianRoot({ + chartType, + Canvas, + data, + config, + children, + stackType = "default", + margins: marginsProp, + className, + animate = true, + animationDuration = 900, + replayToken = 0, + interactive = true, + markerIndex = null, + hovered = false, + bloom = "off", + bloomOnHover = false, + onHoverChange, + defaultSelectedDataKey = null, + onSelectionChange, +}: CartesianChartProps & { + chartType: ChartType + Canvas: ComponentType +}) { + const { ref, size } = useChartDimensions() + const margins = { ...DEFAULT_MARGINS, ...marginsProp } + + const ctx = useChartController({ + chartType, + // Safe: the controller only reads row[key] for the configured series keys. + data: data as Record[], + config, + stackType, + dimensions: size, + margins, + animate, + animationDuration, + replayToken, + markerIndex, + hovered, + bloom, + bloomOnHover, + defaultSelectedDataKey, + onSelectionChange, + }) + + const backChildren: ReactNode[] = [] + const svgChildren: ReactNode[] = [] + const domChildren: ReactNode[] = [] + Children.forEach(children, (child) => { + const layer = layerOf(child) + if (layer === "back") backChildren.push(child) + else if (layer === "dom") domChildren.push(child) + else svgChildren.push(child) + }) + + const onMove = (clientX: number) => { + const el = ref.current + if (!el) return + const rect = el.getBoundingClientRect() + const px = clientX - rect.left - margins.left + const index = ctx.indexAtX(px) + ctx.setHoverIndex(index) + ctx.setCursorX(clientX - rect.left) + onHoverChange?.(index) + } + + return ( + + +
ctx.setMouseInChart(true)} + onPointerMove={interactive ? (e) => onMove(e.clientX) : undefined} + onPointerLeave={() => { + ctx.setMouseInChart(false) + ctx.setHoverIndex(null) + onHoverChange?.(null) + }} + > + {ctx.ready && backChildren.length > 0 && ( + + + {backChildren} + + + )} + + {ctx.ready && ( + + + {svgChildren} + + + )} + {domChildren} +
+
+
+ ) +} + +export type AreaChartProps = CartesianChartProps diff --git a/packages/ui/src/components/dither-kit/chart-context.tsx b/packages/ui/src/components/dither-kit/chart-context.tsx new file mode 100644 index 000000000..6ff4740df --- /dev/null +++ b/packages/ui/src/components/dither-kit/chart-context.tsx @@ -0,0 +1,510 @@ +"use client" + +import type { ScaleLinear } from "d3-scale" +import { createContext, use, useCallback, useMemo, useState } from "react" +import type { CommonChart } from "./common-context" +import type { BloomInput } from "./dither-paint" +import type { DitherColor, Seed } from "./palette" +import { seedOfColor } from "./palette" +import { + buildBandScale, + buildXScale, + buildYScale, + computeBands, + indexAtBand, + nearestIndex, + type StackType, +} from "./scales" +import type { Dimensions } from "./use-chart-dimensions" + +/** Which chart root a part is composed under — drives the boundary guards. */ +export type ChartType = "area" | "bar" | "line" | "pie" | "radar" + +export type ChartConfig = Record + +export type Margins = { + top: number + right: number + bottom: number + left: number +} + +type Row = Record + +export type AreaVariant = "gradient" | "dotted" | "hatched" | "solid" +export type StrokeVariant = "solid" | "dashed" +export type SeriesKind = "area" | "line" | "bar" + +/** What each series part (, , ) registers so the canvas + * knows which series to paint and how. */ +export type SeriesSpec = { + dataKey: string + kind: SeriesKind + variant: AreaVariant + strokeVariant: StrokeVariant +} + +export type ChartContextValue = { + chartType: ChartType // which root this part is under + config: ChartConfig + configKeys: string[] // series order — drives stacking + legend + data: Row[] + dataLength: number + stackType: StackType + + margins: Margins + plot: { width: number; height: number } // inner drawing area + ready: boolean // true once measured (width > 0) + + xCenter: (index: number) => number // category centre px within the plot + bandwidth: number // category slot width (0 for point/area scales) + indexAtX: (px: number) => number // nearest category for a pointer x + // Bar geometry in plot px — one source of truth for the canvas + click rects. + barSlot: ( + index: number, + seriesIndex: number, + seriesCount: number + ) => { x: number; width: number } + y: ScaleLinear // value → px within the plot + bands: Record // per-series [y0, y1] per row + max: number + min: number // most-negative value (0 when nothing dips below the baseline) + + // Interaction state, shared by every part. + selectedDataKey: string | null + selectDataKey: (key: string | null) => void + /** Legend-hover spotlight — dims every series but this one while set. */ + focusDataKey: string | null + setFocusDataKey: (key: string | null) => void + hoverIndex: number | null + setHoverIndex: (index: number | null) => void + markerIndex: number | null // controlled crosshair override (e.g. committed point) + cursorX: number + setCursorX: (px: number) => void + isMouseInChart: boolean + setMouseInChart: (over: boolean) => void + hovered: boolean // parent-driven hover (e.g. the whole card) — lifts the fill + bloom: BloomInput // glow on the dither canvas + bloomOnHover: boolean // only bloom while hovered + + // Series register themselves so the canvas knows what (and how) to paint. + seriesSpecs: Record + registerSeries: (spec: SeriesSpec) => void + unregisterSeries: (dataKey: string) => void + + // Entrance animation (prop-driven). `revision` bumps when the data changes or + // the replay token advances, so the canvas can re-play its entrance. + animate: boolean + animationDuration: number + revision: number + entranceDone: boolean // true once the entrance has played — gates SVG markers + markEntranceDone: () => void // the canvas calls this when its reveal completes + + // Helpers. + seedOf: (key: string) => Seed + common: CommonChart // shared surface for / +} + +const ChartContext = createContext(null) + +const ROOT_OF: Record = { + area: "", + bar: "", + line: "", + pie: "", + radar: "", +} + +/** Generic accessor for internal layers (canvas/overlay) that work for any root. */ +export function useChart() { + const ctx = use(ChartContext) + if (!ctx) { + throw new Error( + "Chart parts must be used within a chart root (e.g. )." + ) + } + return ctx +} + +/** + * Boundary guard for a composable part. Throws a precise error when used outside + * a root, or inside the wrong chart type — e.g. `` placed in an area + * chart. `kind` omitted means the part works under any root (grid, axes, …). + */ +export function useChartPart( + part: string, + kind?: ChartType | ChartType[] +): ChartContextValue { + const ctx = use(ChartContext) + if (!ctx) { + const where = kind + ? ROOT_OF[(Array.isArray(kind) ? kind[0] : kind) ?? "area"] + : "a chart root" + throw new Error(`<${part} /> must be used within ${where}.`) + } + if (kind) { + const allowed = Array.isArray(kind) ? kind : [kind] + if (!allowed.includes(ctx.chartType)) { + throw new Error( + `<${part} /> is not valid inside ${ROOT_OF[ctx.chartType]} — it belongs in ${allowed + .map((k) => ROOT_OF[k]) + .join(" or ")}.` + ) + } + } + return ctx +} + +export { ChartContext } + +/** A counter that advances whenever `data` changes identity or `token` advances + * — drives entrance replays without remounting. Uses the adjust-state-during- + * render pattern (https://react.dev/reference/react/useState) instead of a ref: + * the revision is derived purely from render inputs, so it stays consistent + * across the memoized values below rather than lagging a render behind. */ +export function useRevision(data: unknown, token: number) { + const [prev, setPrev] = useState({ data, token, revision: 0 }) + if (prev.data !== data || prev.token !== token) { + const next = { data, token, revision: prev.revision + 1 } + setPrev(next) + return next.revision + } + return prev.revision +} + +/** + * Builds the shared context value: resolves the plot rect from the measured + * size minus margins, computes the x/y scales and the per-series stack bands, + * and owns the selection + hover state every part reads. + */ +export function useChartController({ + chartType, + data, + config, + stackType, + dimensions, + margins, + animate = true, + animationDuration = 900, + replayToken = 0, + markerIndex = null, + hovered = false, + bloom = "off", + bloomOnHover = false, + defaultSelectedDataKey = null, + onSelectionChange, +}: { + chartType: ChartType + data: Row[] + config: ChartConfig + stackType: StackType + dimensions: Dimensions + margins: Margins + animate?: boolean + animationDuration?: number + replayToken?: number + markerIndex?: number | null + hovered?: boolean + bloom?: BloomInput + bloomOnHover?: boolean + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +}): ChartContextValue { + // This object becomes the ChartContext value, so its identity — and the + // identity of every function/object it carries — must stay stable across + // renders that don't change the underlying inputs. Otherwise every consumer + // (axes, legend, tooltip, dots) re-renders on every parent render. So the + // expensive derivations, the exposed callbacks, and the returned value are + // memoized below; only cheap scalars (bandwidth, ready, plot sizes) are left + // bare, since they're just recomputed reads, not identities anyone depends on. + + // Memoized: configKeys is the dep that drives `bands`, `common` and the + // canvas `targets` memo — a fresh array each render would bust all of them. + const configKeys = useMemo(() => Object.keys(config), [config]) + const revision = useRevision(data, replayToken) + + const [selectedDataKey, setSelectedDataKey] = useState( + defaultSelectedDataKey + ) + const [focusDataKey, setFocusDataKey] = useState(null) + const [hoverIndex, setHoverIndex] = useState(null) + const [cursorX, setCursorX] = useState(0) + const [isMouseInChart, setMouseInChart] = useState(false) + const [seriesSpecs, setSeriesSpecs] = useState>({}) + + // useCallback because the series effects in area.tsx/bar.tsx list these as + // deps — without stable identities the unregister/register effect re-fires + // every render and its setState pair loops ("Maximum update depth exceeded"). + const registerSeries = useCallback((spec: SeriesSpec) => { + setSeriesSpecs((prev) => { + const cur = prev[spec.dataKey] + return cur && + cur.kind === spec.kind && + cur.variant === spec.variant && + cur.strokeVariant === spec.strokeVariant + ? prev + : { ...prev, [spec.dataKey]: spec } + }) + }, []) + const unregisterSeries = useCallback((dataKey: string) => { + setSeriesSpecs((prev) => { + if (!(dataKey in prev)) return prev + const next = { ...prev } + delete next[dataKey] + return next + }) + }, []) + + // Stable so the memoized value keeps its identity; only re-created when the + // caller's selection handler does. + const selectDataKey = useCallback( + (key: string | null) => { + setSelectedDataKey(key) + onSelectionChange?.(key) + }, + [onSelectionChange] + ) + + // The root spreads `{ ...DEFAULT_MARGINS, ...marginsProp }` fresh every + // render, so `margins` never keeps its identity. Pin one off the four numbers + // so it doesn't, on its own, invalidate the value or the plot geometry. + const { top: mTop, right: mRight, bottom: mBottom, left: mLeft } = margins + const stableMargins = useMemo( + () => ({ top: mTop, right: mRight, bottom: mBottom, left: mLeft }), + [mTop, mRight, mBottom, mLeft] + ) + + const plotWidth = Math.max(0, dimensions.width - mLeft - mRight) + const plotHeight = Math.max(0, dimensions.height - mTop - mBottom) + const ready = plotWidth > 0 && plotHeight > 0 + + // The entrance gate flips true when the canvas reveal completes (via + // `markEntranceDone`) so DOM markers fade in with the fill, and re-arms on + // each replay. Adjust-state-during-render instead of an effect, so the reset + // lands in the same render as the revision bump. + const [entrance, setEntrance] = useState({ revision, done: !animate }) + if (entrance.revision !== revision) { + setEntrance({ revision, done: !animate }) + } + const entranceDone = entrance.revision === revision ? entrance.done : !animate + // Stable across renders at the same revision; the canvas holds this in a ref. + const markEntranceDone = useCallback( + () => setEntrance({ revision, done: true }), + [revision] + ) + + // Memoized: the priciest derivation in the render path — it walks every + // row × series to build the stack bands. Hover/cursor state changes must not + // recompute it, only a real data/series/stack change. + const { bands, max, min } = useMemo( + () => computeBands(data, configKeys, stackType), + [data, configKeys, stackType] + ) + + const isBar = chartType === "bar" + // The d3 scale factories are memoized so `y` keeps a stable identity: the + // canvas `targets` memo (cartesian-canvas / bar-canvas) deps on ctx.y, and + // xCenter/indexAtX/barSlot below close over these. + const xPoint = useMemo( + () => buildXScale(data.length, plotWidth), + [data.length, plotWidth] + ) + const xBand = useMemo( + () => buildBandScale(data.length, plotWidth), + [data.length, plotWidth] + ) + const bandwidth = isBar ? xBand.bandwidth() : 0 + const xCenter = useCallback( + (i: number) => + isBar ? (xBand(i) ?? 0) + xBand.bandwidth() / 2 : (xPoint(i) ?? 0), + [isBar, xBand, xPoint] + ) + const indexAtX = useCallback( + (px: number) => + isBar + ? indexAtBand(px, data.length, plotWidth) + : nearestIndex(px, data.length, plotWidth), + [isBar, data.length, plotWidth] + ) + const stacked = stackType === "stacked" || stackType === "percent" + const barSlot = useCallback( + (i: number, si: number, n: number) => { + const center = xCenter(i) + if (stacked) { + const w = bandwidth * 0.9 + return { x: center - w / 2, width: w } + } + const slot = bandwidth / Math.max(n, 1) + return { + x: center - bandwidth / 2 + si * slot + slot * 0.08, + width: slot * 0.84, + } + }, + [xCenter, stacked, bandwidth] + ) + const y = useMemo( + () => buildYScale(min, max, plotHeight), + [min, max, plotHeight] + ) + + // Stable so `common` and the value stay stable; re-created only on config. + const seedOf = useCallback( + (key: string) => seedOfColor(config[key]?.color ?? "grey"), + [config] + ) + + // Memoized: this is the value handed to CommonChartContext (Legend/Tooltip), + // so it needs its own stable identity independent of the parent value. + const common: CommonChart = useMemo(() => ({ + names: configKeys, + labelOf: (n) => config[n]?.label ?? n, + seedOf, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + ready, + tooltipLeft: Math.max(48, Math.min(plotWidth + mLeft - 48, cursorX)), + // Follow the highest hovered node so the card rides the data path, but + // keep enough headroom that the upward-lifted card never clips the top. + tooltipTop: (() => { + const floor = mTop + 44 + if (hoverIndex == null) return floor + let minY = Number.POSITIVE_INFINITY + for (const key of configKeys) { + const b = bands[key]?.[hoverIndex] + if (b) minY = Math.min(minY, y(b[1])) + } + if (!Number.isFinite(minY)) return floor + return Math.max(floor, mTop + minY) + })(), + heading: (i, labelKey) => + labelKey ? String(data[i]?.[labelKey] ?? "") : null, + itemsAt: (i) => + configKeys.map((name) => { + const raw = data[i]?.[name] + return { + name, + label: config[name]?.label ?? name, + value: typeof raw === "number" ? raw : 0, + seed: seedOf(name), + dimmed: (() => { + const emphasis = selectedDataKey ?? focusDataKey + return emphasis !== null && emphasis !== name + })(), + } + }), + }), [ + configKeys, + config, + seedOf, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + ready, + plotWidth, + mLeft, + mTop, + cursorX, + bands, + y, + data, + ]) + + // Memoized: this is the ChartContext value. A fresh object here would + // re-render every consumer on every parent render — the whole reason the + // pieces above are stabilized. Rebuilds only when a listed input changes + // (which is exactly when a consumer needs the update). The useState setters + // are listed but never change identity, so they never trigger a rebuild. + return useMemo( + () => ({ + chartType, + config, + configKeys, + data, + dataLength: data.length, + stackType, + margins: stableMargins, + plot: { width: plotWidth, height: plotHeight }, + ready, + xCenter, + bandwidth, + indexAtX, + barSlot, + y, + bands, + max, + min, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + setHoverIndex, + markerIndex, + cursorX, + setCursorX, + isMouseInChart, + setMouseInChart, + hovered, + bloom, + bloomOnHover, + seriesSpecs, + registerSeries, + unregisterSeries, + animate, + animationDuration, + revision, + entranceDone, + markEntranceDone, + seedOf, + common, + }), + [ + chartType, + config, + configKeys, + data, + stackType, + stableMargins, + plotWidth, + plotHeight, + ready, + xCenter, + bandwidth, + indexAtX, + barSlot, + y, + bands, + max, + min, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + setHoverIndex, + markerIndex, + cursorX, + setCursorX, + isMouseInChart, + setMouseInChart, + hovered, + bloom, + bloomOnHover, + seriesSpecs, + registerSeries, + unregisterSeries, + animate, + animationDuration, + revision, + entranceDone, + markEntranceDone, + seedOf, + common, + ] + ) +} diff --git a/packages/ui/src/components/dither-kit/common-context.tsx b/packages/ui/src/components/dither-kit/common-context.tsx new file mode 100644 index 000000000..dae0cc208 --- /dev/null +++ b/packages/ui/src/components/dither-kit/common-context.tsx @@ -0,0 +1,48 @@ +"use client" + +import { createContext, use } from "react" +import type { Seed } from "./palette" + +/** A single tooltip row — one series (cartesian/radar) or one slice (pie). */ +export type TooltipItem = { + name: string + label: string + value: number + seed: Seed + dimmed: boolean +} + +/** + * The minimal surface shared by every chart family, so `` and + * `` work identically whether they sit in a cartesian, bar, or polar + * root. Each root publishes one of these alongside its family-specific context. + */ +export type CommonChart = { + names: string[] // legend entries — series keys (cartesian) or slice names (pie) + labelOf: (name: string) => string + seedOf: (name: string) => Seed + selectedDataKey: string | null + selectDataKey: (key: string | null) => void + /** Transient legend-hover emphasis — spotlights one series (others dim) + * while the pointer rests on its legend entry. Selection still wins. */ + focusDataKey: string | null + setFocusDataKey: (key: string | null) => void + hoverIndex: number | null + heading: (index: number, labelKey?: string) => string | null + itemsAt: (index: number) => TooltipItem[] + ready: boolean + tooltipLeft: number // clamped px for the floating tooltip + tooltipTop: number // px — follows the hovered node (cartesian) / cursor (polar) +} + +export const CommonChartContext = createContext(null) + +export function useCommonChart() { + const ctx = use(CommonChartContext) + if (!ctx) { + throw new Error( + " / must be used within a chart root." + ) + } + return ctx +} diff --git a/packages/ui/src/components/dither-kit/dither-paint.ts b/packages/ui/src/components/dither-kit/dither-paint.ts new file mode 100644 index 000000000..0bdd5a9ec --- /dev/null +++ b/packages/ui/src/components/dither-kit/dither-paint.ts @@ -0,0 +1,177 @@ +import type { AreaVariant } from "./chart-context" +import { rgb, type Seed } from "./palette" + +// 4×4 ordered (Bayer) matrix, normalized to 0–1 thresholds — the exact matrix +// the legacy chart dithers with. +export const BAYER = [ + [0, 8, 2, 10], + [12, 4, 14, 6], + [3, 11, 1, 9], + [15, 7, 13, 5], +].map((row) => row.map((v) => (v + 0.5) / 16)) + +export const CELL = 2 // css px per dither cell — chunky enough to read pixelated +export const MAX_COLS = 520 +export const MAX_ROWS = 200 +// Opacity of the top border outline (just under solid, so it reads as a soft +// edge rather than a hard line). See the note on colour vs opacity below. +export const BORDER_ALPHA = 0.72 +// Opacity of a dither "off" cell relative to an "on" cell. The scatter modulates +// between these two tiers of the *same* colour instead of leaving holes, so the +// background never shows through as stark white on a light theme. +export const OFF_TIER = 0.4 + +export type PaintOpts = { + variant: AreaVariant + intensity: number // 0–1 hover lift + dim: number // selection dim multiplier (0.3 dimmed, 1 normal) + stacked: boolean // denser + solid floor when layers stack + sparse?: number // raise the dither threshold (thin out) — front layers +} + +// Colour vs opacity — the guiding rule for the whole engine: +// +// Work with opacities instead of different shades of the same color. This will +// make sure it looks good on both light and dark mode. +// +// So every pixel is the series' single `fill` colour and we vary only its alpha. +// The old lighter `line` / near-white `star` shades were dropped: a shade that +// pops on a dark background reads as a jarring bright speck on a light one, while +// the same colour at a lower opacity simply blends into whatever sits behind it. + +/** + * Fill one backing-canvas column `x` from row `top` down to `floor` with the + * ordered-dither scatter — solid at the floor, dissolving upward so it *fades + * out toward the value line* — then cap the top with a soft border outline in + * the series colour. Density drives opacity (see the note above), so the fade + * reads correctly against both light and dark backgrounds. The single source of + * the dither look across area / line / bar. + */ +export function paintColumn( + octx: CanvasRenderingContext2D, + x: number, + top: number, + floor: number, + seed: Seed, + { variant, intensity, dim, stacked, sparse = 0 }: PaintOpts +) { + const t = Math.round(top) + const f = Math.round(floor) + const depth = f - t + if (depth <= 0) { + octx.fillStyle = rgb(seed.fill, 1, BORDER_ALPHA * dim) + octx.fillRect(x, t, 1, 1) + return + } + const bias = (variant === "dotted" ? 0.12 : 0) + (stacked ? 0.2 : 0) - sparse + for (let y = t; y < f; y++) { + // Inverted falloff: 0 at the top line, 1 at the floor — dense at the + // bottom, thinning as it rises toward the outline. + let density = (y - t) / depth + if (stacked) density = 0.5 + 0.5 * density + if (variant === "hatched" && ((x + y) & 3) >= 2) continue + const lit = + variant === "solid" || + density > (BAYER[y & 3]?.[x & 3] ?? 0) - 0.1 * intensity - bias + // "dotted" keeps real gaps for its open look; every other variant covers + // the cell and lets the dither ride the alpha (on = full tier, off = a + // faint tint) so nothing shows the background through as white. + if (variant === "dotted" && !lit) continue + // Density → alpha (see the colour-vs-opacity note above). + const k = (0.3 + density * 0.7) * (1 + 0.22 * intensity) + const alpha = clamp01((lit ? k : k * OFF_TIER) * dim) + octx.fillStyle = rgb(seed.fill, 1, alpha) + octx.fillRect(x, y, 1, 1) + } + // Top border outline — the shape's edge now that the fill fades out here. + // Kept just under full opacity, with a faint feather row beneath, so it reads + // as a soft edge rather than a hard line floating over the fade. + octx.fillStyle = rgb(seed.fill, 1, BORDER_ALPHA * dim) + octx.fillRect(x, t, 1, 1) + if (depth > 1) { + octx.fillStyle = rgb(seed.fill, 1, BORDER_ALPHA * 0.5 * dim) + octx.fillRect(x, t + 1, 1, 1) + } +} + +/** Linear-resample a per-index fraction array to `cols` columns. */ +export function resample(src: number[], cols: number): number[] { + const out = new Array(cols) + const last = Math.max(src.length - 1, 1) + for (let c = 0; c < cols; c++) { + const t = (c / Math.max(cols - 1, 1)) * last + const i = Math.floor(t) + const f = t - i + const a = src[i] ?? 0 + const b = src[Math.min(i + 1, src.length - 1)] ?? a + out[c] = a + (b - a) * f + } + return out +} + +/** Backing-canvas resolution for a plot rect — low-res, scaled up `pixelated`. */ +export function backingSize(width: number, height: number) { + return { + cols: Math.min(MAX_COLS, Math.max(8, Math.round(width / CELL))), + rows: Math.min(MAX_ROWS, Math.max(8, Math.round(height / CELL))), + } +} + +// Bloom — a real "shader" glow that comes from the colours themselves: a blurred +// copy of the rendered canvas, composited additively (`plus-lighter`) so each +// hue blooms in its own colour instead of a grey wash. Lives on a second canvas +// layered over the crisp one (which stays sharp/pixelated). +export type BloomLevel = "off" | "low" | "high" | "aura" +export type BloomBlend = "plus-lighter" | "screen" | "lighten" +export type BloomConfig = { + blur: number // px + brightness: number // 1 = none + opacity: number // 0–1 + /** Saturation of the glow — >1 keeps it vividly in the dither's colour + * instead of washing toward white. */ + saturate?: number + blend?: BloomBlend // additive by default +} +/** A preset name, a full config, or "off". */ +export type BloomInput = BloomLevel | BloomConfig + +const PRESET: Record, BloomConfig> = { + low: { blur: 3, brightness: 1.35, opacity: 0.7, saturate: 1.4 }, + high: { blur: 5, brightness: 1.5, opacity: 0.78, saturate: 1.5 }, + aura: { blur: 15, brightness: 2.9, opacity: 0.1, saturate: 3 }, +} + +export type BloomStyle = { + filter: string + opacity: number + mixBlendMode: BloomBlend + imageRendering: "auto" +} + +/** Style for the bloom *layer* canvas (a blurred, additive copy). null when off. */ +export function bloomLayerStyle( + input: BloomInput, + active: boolean +): BloomStyle | null { + if (!active || input === "off") return null + const cfg = typeof input === "string" ? PRESET[input] : input + return { + filter: `blur(${cfg.blur}px) brightness(${cfg.brightness}) saturate(${cfg.saturate ?? 1})`, + opacity: cfg.opacity, + mixBlendMode: cfg.blend ?? "plus-lighter", + imageRendering: "auto", + } +} + +// Easing — gentle start + soft settle so entrances don't feel linear. +export const easeInOutCubic = (t: number) => + t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2 +export const easeOutCubic = (t: number) => 1 - (1 - t) ** 3 +export const clamp01 = (t: number) => (t < 0 ? 0 : t > 1 ? 1 : t) + +/** Whether the OS asks for reduced motion (snap + steady stars). */ +export function prefersReducedMotion() { + return ( + window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false + ) +} diff --git a/packages/ui/src/components/dither-kit/dot.tsx b/packages/ui/src/components/dither-kit/dot.tsx new file mode 100644 index 000000000..07eb2ea30 --- /dev/null +++ b/packages/ui/src/components/dither-kit/dot.tsx @@ -0,0 +1,90 @@ +"use client" + +import { useChart } from "./chart-context" +import { rgb, type Seed } from "./palette" +import { useSeries } from "./series-context" + +export type DotVariant = "border" | "colored-border" | "filled" + +function dotPaint(variant: DotVariant, seed: Seed) { + switch (variant) { + case "colored-border": + return { + fill: "var(--card, #0b0b0c)", + stroke: rgb(seed.line), + strokeWidth: 1.5, + } + case "filled": + return { fill: rgb(seed.star), stroke: rgb(seed.line), strokeWidth: 1 } + default: + return { + fill: "var(--card, #0b0b0c)", + stroke: rgb(seed.star, 0.8), + strokeWidth: 1, + } + } +} + +/** A marker at every data point along the series' top line. */ +export function Dot({ + variant = "border", + r = 2, +}: { + variant?: DotVariant + r?: number +}) { + const ctx = useChart() + const { dataKey, seed } = useSeries("Dot") + const band = ctx.bands[dataKey] + if (!ctx.ready || !band) return null + const paint = dotPaint(variant, seed) + + return ( + // Fade in once the fill has drawn so dots don't float over the entrance. + + {band.map((b, i) => ( + + ))} + + ) +} + +/** A single marker at the hovered point — keys off the shared hover index. */ +export function ActiveDot({ + variant = "colored-border", + r = 3, +}: { + variant?: DotVariant + r?: number +}) { + const ctx = useChart() + const { dataKey, seed } = useSeries("ActiveDot") + const band = ctx.bands[dataKey] + if (!ctx.ready || !band || ctx.hoverIndex == null || !ctx.entranceDone) + return null + const b = band[ctx.hoverIndex] + if (!b) return null + const paint = dotPaint(variant, seed) + const cx = ctx.xCenter(ctx.hoverIndex) + const cy = ctx.y(b[1]) + + return ( + + {/* Soft halo so the active point is unmistakable over the dither. */} + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/grid.tsx b/packages/ui/src/components/dither-kit/grid.tsx new file mode 100644 index 000000000..b781d788e --- /dev/null +++ b/packages/ui/src/components/dither-kit/grid.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useChartPart } from "./chart-context" + +export function Grid({ + horizontal = true, + vertical = false, + strokeDasharray = "3 3", +}: { + horizontal?: boolean + vertical?: boolean + strokeDasharray?: string +}) { + const ctx = useChartPart("Grid") + if (!ctx.ready) return null + const { width } = ctx.plot + + return ( + + {horizontal && + ctx.y + .ticks(4) + .map((t) => ( + + ))} + {vertical && + ctx.data.map((_, i) => ( + + ))} + + ) +} + +// Render beneath the dither canvas so grid lines sit behind the fill. +Grid.chartLayer = "back" as const diff --git a/packages/ui/src/components/dither-kit/legend.tsx b/packages/ui/src/components/dither-kit/legend.tsx new file mode 100644 index 000000000..f6ced6823 --- /dev/null +++ b/packages/ui/src/components/dither-kit/legend.tsx @@ -0,0 +1,69 @@ +"use client" + +import { useCommonChart } from "./common-context" +import { cn } from "./lib" +import { rgb } from "./palette" + +/** Series/slice legend. With `isClickable`, each entry toggles its selection. + * Works in every chart family via the shared common context. + * + * Note: this is an absolute overlay pinned to the top of the plot, so it's best + * for ≤2–3 entries. With more entries (or a narrow container) it wraps onto + * extra rows that overlay the chart — reach for the in-flow `` + * instead, which renders as a sibling and can't overlap at any width. */ +export function Legend({ + isClickable = false, + align = "right", +}: { + isClickable?: boolean + align?: "left" | "center" | "right" +}) { + const chart = useCommonChart() + + return ( +
+ {chart.names.map((name) => { + const seed = chart.seedOf(name) + const emphasis = chart.selectedDataKey ?? chart.focusDataKey + const dimmed = emphasis !== null && emphasis !== name + return ( + + ) + })} +
+ ) +} + +Legend.chartLayer = "dom" as const diff --git a/packages/ui/src/components/dither-kit/lib.ts b/packages/ui/src/components/dither-kit/lib.ts new file mode 100644 index 000000000..da71254e2 --- /dev/null +++ b/packages/ui/src/components/dither-kit/lib.ts @@ -0,0 +1,8 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +/** Tailwind-aware className combiner — local copy so the chart pack is + * self-contained and portable as a registry. */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/ui/src/components/dither-kit/palette.ts b/packages/ui/src/components/dither-kit/palette.ts new file mode 100644 index 000000000..93faa58ea --- /dev/null +++ b/packages/ui/src/components/dither-kit/palette.ts @@ -0,0 +1,40 @@ +export type Rgb = [number, number, number] + +export type DitherColor = + | "green" + | "blue" + | "purple" + | "pink" + | "orange" + | "red" + | "grey" + +export type Seed = { fill: Rgb; line: Rgb; star: Rgb } + +// Each seed: the area-fill hue, the bright series line, and the star sparkle. +export const PALETTE: Record = { + green: { fill: [40, 210, 110], line: [150, 255, 180], star: [200, 255, 220] }, + blue: { fill: [53, 143, 243], line: [150, 200, 255], star: [205, 228, 255] }, + purple: { + fill: [150, 110, 255], + line: [200, 175, 255], + star: [225, 210, 255], + }, + pink: { fill: [240, 90, 190], line: [255, 170, 220], star: [255, 205, 235] }, + orange: { + fill: [255, 150, 50], + line: [255, 195, 130], + star: [255, 220, 175], + }, + red: { fill: [240, 70, 70], line: [255, 150, 140], star: [255, 195, 185] }, + // No-data: a muted grey so empty metrics read as "nothing here". + grey: { fill: [92, 92, 100], line: [140, 140, 150], star: [165, 165, 175] }, +} + +export const rgb = ([r, g, b]: Rgb, k = 1, a = 1) => + `rgba(${Math.round(r * k)},${Math.round(g * k)},${Math.round(b * k)},${a})` + +export const seedOfColor = (color: DitherColor): Seed => PALETTE[color] + +export const isDitherColor = (value: unknown): value is DitherColor => + typeof value === "string" && value in PALETTE 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/pixel.ts b/packages/ui/src/components/dither-kit/pixel.ts new file mode 100644 index 000000000..3a0dbef66 --- /dev/null +++ b/packages/ui/src/components/dither-kit/pixel.ts @@ -0,0 +1,109 @@ +import { type DitherColor, PALETTE, type Rgb } from "./palette" + +// 4×4 ordered (Bayer) matrix, normalized to 0–1 thresholds — the same matrix +// the charts dither with. +export const BAYER4 = [ + [0, 8, 2, 10], + [12, 4, 14, 6], + [3, 11, 1, 9], + [15, 7, 13, 5], +].map((row) => row.map((v) => (v + 0.5) / 16)) + +export const clamp01 = (t: number) => (t < 0 ? 0 : t > 1 ? 1 : t) + +/** 32-bit FNV-1a hash — turns any string seed into a stable uint32. */ +export function fnv1a(str: string): number { + let h = 0x811c9dc5 + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i) + h = Math.imul(h, 0x01000193) + } + return h >>> 0 +} + +/** Tiny deterministic PRNG (xorshift32) — returns floats in [0, 1). */ +export function xorshift32(seed: number): () => number { + let s = seed || 0x9e3779b9 + return () => { + s ^= s << 13 + s >>>= 0 + s ^= s >>> 17 + s ^= s << 5 + s >>>= 0 + return s / 0x100000000 + } +} + +/** A named palette colour or a raw hue (0–360). */ +export type PixelColor = DitherColor | number + +/** Hue (0–360) → an rgb fill tuned to sit alongside the chart palette. */ +export function hueFill(hue: number): Rgb { + const h = ((hue % 360) + 360) % 360 + const s = 0.85 + const l = 0.58 + const c = (1 - Math.abs(2 * l - 1)) * s + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) + const m = l - c / 2 + const [r, g, b] = + h < 60 + ? [c, x, 0] + : h < 120 + ? [x, c, 0] + : h < 180 + ? [0, c, x] + : h < 240 + ? [0, x, c] + : h < 300 + ? [x, 0, c] + : [c, 0, x] + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255), + ] +} + +/** Resolve a {@link PixelColor} to its rgb fill. */ +export function fillOf(color: PixelColor): Rgb { + return typeof color === "number" ? hueFill(color) : PALETTE[color].fill +} + +// Bloom — same recipe as the charts: a blurred copy of the crisp canvas, +// composited additively so the glow stays in the dither's own colour. +export type PixelBloom = "off" | "low" | "high" | "aura" + +const BLOOM_PRESET: Record< + Exclude, + { blur: number; brightness: number; opacity: number; saturate: number } +> = { + low: { blur: 3, brightness: 1.35, opacity: 0.7, saturate: 1.4 }, + high: { blur: 5, brightness: 1.5, opacity: 0.78, saturate: 1.5 }, + aura: { blur: 15, brightness: 2.9, opacity: 0.1, saturate: 3 }, +} + +export type PixelBloomStyle = { + filter: string + opacity: number + mixBlendMode: "plus-lighter" + imageRendering: "auto" +} + +/** Style for the bloom layer canvas. null when off. */ +export function pixelBloomStyle(bloom: PixelBloom): PixelBloomStyle | null { + if (bloom === "off") return null + const cfg = BLOOM_PRESET[bloom] + return { + filter: `blur(${cfg.blur}px) brightness(${cfg.brightness}) saturate(${cfg.saturate})`, + opacity: cfg.opacity, + mixBlendMode: "plus-lighter", + imageRendering: "auto", + } +} + +/** Whether the OS asks for reduced motion (skip entrances). */ +export function pixelPrefersReducedMotion(): boolean { + return ( + window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false + ) +} diff --git a/packages/ui/src/components/dither-kit/polar-context.tsx b/packages/ui/src/components/dither-kit/polar-context.tsx new file mode 100644 index 000000000..0fd06b32d --- /dev/null +++ b/packages/ui/src/components/dither-kit/polar-context.tsx @@ -0,0 +1,382 @@ +"use client" + +import { createContext, use, useCallback, useMemo, useState } from "react" +import { + type AreaVariant, + type ChartConfig, + type ChartType, + type Margins, + useRevision, +} from "./chart-context" +import type { CommonChart } from "./common-context" +import type { BloomInput } from "./dither-paint" +import type { Seed } from "./palette" +import { seedOfColor } from "./palette" +import { type PieSlice, pieSlices, type RadarAxis, radarAxes } from "./polar" +import type { Dimensions } from "./use-chart-dimensions" + +type Row = Record + +const ROOT_OF: Record = { + pie: "", + radar: "", +} + +export type PolarChartContextValue = { + chartType: ChartType + config: ChartConfig + configKeys: string[] + data: Row[] + dataLength: number + ready: boolean + plot: { width: number; height: number } + margins: Margins + center: { x: number; y: number } + outerRadius: number + innerRadius: number + animate: boolean + animationDuration: number + revision: number + bloom: BloomInput + bloomOnHover: boolean + + seedOf: (key: string) => Seed + variantOf: (key: string) => AreaVariant + registerVariant: (key: string, variant: AreaVariant) => void + unregisterVariant: (key: string) => void + + selectedDataKey: string | null + selectDataKey: (key: string | null) => void + /** Legend-hover spotlight — dims every series but this one while set. */ + focusDataKey: string | null + setFocusDataKey: (key: string | null) => void + hoverIndex: number | null + setHoverIndex: (i: number | null) => void + setCursor: (px: number, py: number) => void + isMouseInChart: boolean + setMouseInChart: (over: boolean) => void + + pie: PieSlice[] | null // present for pie charts + radar: { axes: RadarAxis[]; max: number } | null // present for radar charts + + common: CommonChart +} + +const PolarChartContext = createContext(null) + +export function usePolarChart() { + const ctx = use(PolarChartContext) + if (!ctx) { + throw new Error("Polar chart parts must be used within a polar chart root.") + } + return ctx +} + +/** Boundary guard for polar parts (``, ``). */ +export function usePolarPart(part: string, kind: "pie" | "radar") { + const ctx = use(PolarChartContext) + if (!ctx) { + throw new Error(`<${part} /> must be used within ${ROOT_OF[kind]}.`) + } + if (ctx.chartType !== kind) { + throw new Error( + `<${part} /> is not valid inside ${ROOT_OF[ctx.chartType]} — it belongs in ${ROOT_OF[kind]}.` + ) + } + return ctx +} + +export { PolarChartContext } + +export function usePolarController({ + chartType, + data, + config, + dataKey, + nameKey, + innerRadiusRatio, + dimensions, + margins, + animate = true, + animationDuration = 900, + replayToken = 0, + bloom = "off", + bloomOnHover = false, + defaultSelectedDataKey = null, + onSelectionChange, +}: { + chartType: "pie" | "radar" + data: Row[] + config: ChartConfig + dataKey: string + nameKey: string + innerRadiusRatio: number + dimensions: Dimensions + margins: Margins + animate?: boolean + animationDuration?: number + replayToken?: number + bloom?: BloomInput + bloomOnHover?: boolean + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +}): PolarChartContextValue { + // This object becomes the PolarChartContext value, so its identity — and the + // identity of every function/object it carries — must stay stable across + // renders that don't change the inputs; otherwise every consumer (legend, + // tooltip, slices, axes) re-renders on every parent render. The expensive + // derivations, exposed callbacks, and returned value are memoized below; + // cheap scalars (radii, ready) are left bare as plain recomputed reads. + + // Memoized: drives `pie`/`radar`/`common` — a fresh array would bust them. + const configKeys = useMemo(() => Object.keys(config), [config]) + const revision = useRevision(data, replayToken) + + const [selectedDataKey, setSelectedDataKey] = useState( + defaultSelectedDataKey + ) + const [focusDataKey, setFocusDataKey] = useState(null) + const [hoverIndex, setHoverIndex] = useState(null) + const [cursorX, setCursorX] = useState(0) + const [cursorY, setCursorY] = useState(0) + const [isMouseInChart, setMouseInChart] = useState(false) + // Stable (only wraps two useState setters) so the value keeps its identity. + const setCursor = useCallback((px: number, py: number) => { + setCursorX(px) + setCursorY(py) + }, []) + const [variants, setVariants] = useState>({}) + + // useCallback for the same reason as registerSeries in chart-context.tsx: + // pie.tsx/radar.tsx list these as effect deps, so without stable identities + // the unregister/register effect re-fires and its setState pair loops. + const registerVariant = useCallback((key: string, variant: AreaVariant) => { + setVariants((prev) => + prev[key] === variant ? prev : { ...prev, [key]: variant } + ) + }, []) + const unregisterVariant = useCallback((key: string) => { + setVariants((prev) => { + if (!(key in prev)) return prev + const next = { ...prev } + delete next[key] + return next + }) + }, []) + + // Stable so the value keeps its identity; re-created only on config change. + const selectDataKey = useCallback( + (key: string | null) => { + setSelectedDataKey(key) + onSelectionChange?.(key) + }, + [onSelectionChange] + ) + + // The root spreads margins fresh every render; pin a stable object off the + // four numbers so it doesn't, on its own, invalidate the value. + const { top: mTop, right: mRight, bottom: mBottom, left: mLeft } = margins + const stableMargins = useMemo( + () => ({ top: mTop, right: mRight, bottom: mBottom, left: mLeft }), + [mTop, mRight, mBottom, mLeft] + ) + + const plotWidth = Math.max(0, dimensions.width - mLeft - mRight) + const plotHeight = Math.max(0, dimensions.height - mTop - mBottom) + const ready = plotWidth > 0 && plotHeight > 0 + const pad = chartType === "radar" ? 20 : 6 + const outerRadius = Math.max(0, Math.min(plotWidth, plotHeight) / 2 - pad) + const innerRadius = chartType === "pie" ? outerRadius * innerRadiusRatio : 0 + const centerX = plotWidth / 2 + const centerY = plotHeight / 2 + + // Stable so `common` and the value stay stable; re-created only on config. + const seedOf = useCallback( + (key: string) => seedOfColor(config[key]?.color ?? "grey"), + [config] + ) + // "*" is the pie-wide variant set by ; radar registers per series key. + const variantOf = useCallback( + (key: string) => variants[key] ?? variants["*"] ?? "gradient", + [variants] + ) + + // Memoized: slice geometry — recomputing it on every hover/cursor tick would + // rebuild the pie layout needlessly. + const pie = useMemo( + () => (chartType === "pie" ? pieSlices(data, dataKey, nameKey) : null), + [chartType, data, dataKey, nameKey] + ) + + // Memoized: walks every row × series for the axis max, then builds the axes. + const radar = useMemo(() => { + if (chartType !== "radar") return null + let max = 0 + for (const row of data) { + for (const key of configKeys) { + const v = Number(row[key]) || 0 + if (v > max) max = v + } + } + return { axes: radarAxes(data, nameKey), max: max || 1 } + }, [chartType, data, configKeys, nameKey]) + + // Memoized: this is the value handed to CommonChartContext (Legend/Tooltip), + // so it needs its own stable identity independent of the parent value. + const common: CommonChart = useMemo(() => { + const tooltipLeft = Math.max(48, Math.min(plotWidth + mLeft - 48, cursorX)) + const tooltipTop = Math.max(mTop + 44, cursorY) + const emphasis = selectedDataKey ?? focusDataKey + if (chartType === "pie" && pie) { + const names = pie.map((s) => s.name) + return { + names, + tooltipTop, + labelOf: (n) => config[n]?.label ?? n, + seedOf, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + ready, + tooltipLeft, + heading: (i) => pie[i]?.name ?? null, + itemsAt: (i) => { + const s = pie[i] + if (!s) return [] + return [ + { + name: s.name, + label: config[s.name]?.label ?? s.name, + value: s.value, + seed: seedOf(s.name), + dimmed: emphasis !== null && emphasis !== s.name, + }, + ] + }, + } + } + // radar + return { + names: configKeys, + tooltipTop, + labelOf: (n) => config[n]?.label ?? n, + seedOf, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + ready, + tooltipLeft, + heading: (i) => radar?.axes[i]?.label ?? null, + itemsAt: (i) => + configKeys.map((name) => { + const raw = data[i]?.[name] + return { + name, + label: config[name]?.label ?? name, + value: typeof raw === "number" ? raw : 0, + seed: seedOf(name), + dimmed: emphasis !== null && emphasis !== name, + } + }), + } + }, [ + chartType, + config, + configKeys, + data, + pie, + radar, + seedOf, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + ready, + plotWidth, + mLeft, + mTop, + cursorX, + cursorY, + ]) + + // Memoized: this is the PolarChartContext value. A fresh object here would + // re-render every consumer on every parent render — the reason the pieces + // above are stabilized. Rebuilds only when a listed input changes. The + // useState setters are listed but never change identity. + return useMemo( + () => ({ + chartType, + config, + configKeys, + data, + dataLength: data.length, + ready, + plot: { width: plotWidth, height: plotHeight }, + margins: stableMargins, + center: { x: centerX, y: centerY }, + outerRadius, + innerRadius, + animate, + animationDuration, + revision, + bloom, + bloomOnHover, + seedOf, + variantOf, + registerVariant, + unregisterVariant, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + setHoverIndex, + setCursor, + isMouseInChart, + setMouseInChart, + pie, + radar, + common, + }), + [ + chartType, + config, + configKeys, + data, + ready, + plotWidth, + plotHeight, + stableMargins, + centerX, + centerY, + outerRadius, + innerRadius, + animate, + animationDuration, + revision, + bloom, + bloomOnHover, + seedOf, + variantOf, + registerVariant, + unregisterVariant, + selectedDataKey, + selectDataKey, + focusDataKey, + setFocusDataKey, + hoverIndex, + setHoverIndex, + setCursor, + isMouseInChart, + setMouseInChart, + pie, + radar, + common, + ] + ) +} diff --git a/packages/ui/src/components/dither-kit/polar-root.tsx b/packages/ui/src/components/dither-kit/polar-root.tsx new file mode 100644 index 000000000..0b393778b --- /dev/null +++ b/packages/ui/src/components/dither-kit/polar-root.tsx @@ -0,0 +1,173 @@ +"use client" + +import { + Children, + type ComponentType, + isValidElement, + type ReactNode, +} from "react" +import type { ChartConfig, Margins } from "./chart-context" +import { CommonChartContext } from "./common-context" +import type { BloomInput } from "./dither-paint" +import { cn } from "./lib" +import { axisAtAngle, sliceAtAngle } from "./polar" +import { PolarChartContext, usePolarController } from "./polar-context" +import { useChartDimensions } from "./use-chart-dimensions" + +// `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 + +const DEFAULT_POLAR_MARGINS: Margins = { + top: 22, + right: 14, + bottom: 14, + left: 14, +} + +function layerOf(node: ReactNode): "back" | "dom" | "svg" { + if (!isValidElement(node) || typeof node.type === "string") return "svg" + return (node.type as { chartLayer?: "back" | "dom" }).chartLayer ?? "svg" +} + +export type PolarRootProps = { + chartType: "pie" | "radar" + /** Family painter — `PieCanvas` or `RadarCanvas`; ships with each chart. */ + Canvas: ComponentType + /** Extra back-layer SVG content (e.g. the radar frame). */ + backDecoration?: ReactNode + data: TData[] + config: ChartConfig + children: ReactNode + dataKey: string + nameKey: string + innerRadius?: number // 0–1 ratio (donut); pie only + margins?: Partial + className?: string + animate?: boolean + animationDuration?: number + replayToken?: number + bloom?: BloomInput + bloomOnHover?: boolean + defaultSelectedDataKey?: string | null + onSelectionChange?: (key: string | null) => void +} + +export function PolarRoot({ + chartType, + Canvas, + backDecoration, + data, + config, + children, + dataKey, + nameKey, + innerRadius = 0, + margins: marginsProp, + className, + animate = true, + animationDuration = 900, + replayToken = 0, + bloom = "off", + bloomOnHover = false, + defaultSelectedDataKey = null, + onSelectionChange, +}: PolarRootProps) { + const { ref, size } = useChartDimensions() + const margins = { ...DEFAULT_POLAR_MARGINS, ...marginsProp } + + const ctx = usePolarController({ + chartType, + // Safe: the controller only reads row[key] for the configured keys. + data: data as Record[], + config, + dataKey, + nameKey, + innerRadiusRatio: innerRadius, + dimensions: size, + margins, + animate, + animationDuration, + replayToken, + bloom, + bloomOnHover, + defaultSelectedDataKey, + onSelectionChange, + }) + + const backChildren: ReactNode[] = [] + const svgChildren: ReactNode[] = [] + const domChildren: ReactNode[] = [] + Children.forEach(children, (child) => { + const layer = layerOf(child) + if (layer === "back") backChildren.push(child) + else if (layer === "dom") domChildren.push(child) + else svgChildren.push(child) + }) + + const onMove = (clientX: number, clientY: number) => { + const el = ref.current + if (!el) return + const rect = el.getBoundingClientRect() + const dx = clientX - rect.left - margins.left - ctx.center.x + const dy = clientY - rect.top - margins.top - ctx.center.y + const angle = Math.atan2(dy, dx) + const r = Math.hypot(dx, dy) + if (chartType === "pie" && ctx.pie) { + const inside = r <= ctx.outerRadius && r >= ctx.innerRadius + const i = inside ? sliceAtAngle(ctx.pie, angle) : -1 + ctx.setHoverIndex(i >= 0 ? i : null) + } else if (ctx.radar) { + ctx.setHoverIndex(axisAtAngle(ctx.radar.axes, angle)) + } + ctx.setCursor(clientX - rect.left, clientY - rect.top) + } + + return ( + + +
ctx.setMouseInChart(true)} + onPointerMove={(e) => onMove(e.clientX, e.clientY)} + onPointerLeave={() => { + ctx.setMouseInChart(false) + ctx.setHoverIndex(null) + }} + > + {ctx.ready && ( + + + {backDecoration} + {backChildren} + + + )} + + {ctx.ready && ( + + + {svgChildren} + + + )} + {domChildren} +
+
+
+ ) +} diff --git a/packages/ui/src/components/dither-kit/polar.ts b/packages/ui/src/components/dither-kit/polar.ts new file mode 100644 index 000000000..d92a80741 --- /dev/null +++ b/packages/ui/src/components/dither-kit/polar.ts @@ -0,0 +1,122 @@ +type Row = Record + +const TOP = -Math.PI / 2 +const TAU = Math.PI * 2 + +export type PieSlice = { + name: string + value: number + start: number // radians + end: number + mid: number +} + +/** Slice angles from each data row's value under `dataKey`, named by `nameKey`. */ +export function pieSlices( + data: Row[], + dataKey: string, + nameKey: string +): PieSlice[] { + const vals = data.map((r) => Math.max(0, Number(r[dataKey]) || 0)) + const total = vals.reduce((a, b) => a + b, 0) || 1 + let a = TOP + return data.map((r, i) => { + const span = ((vals[i] ?? 0) / total) * TAU + const slice = { + name: String(r[nameKey] ?? i), + value: vals[i] ?? 0, + start: a, + end: a + span, + mid: a + span / 2, + } + a += span + return slice + }) +} + +/** Which slice a pointer angle falls in (or -1). */ +export function sliceAtAngle(slices: PieSlice[], angle: number): number { + // Normalize so comparisons against [start, end) (which begin at TOP) work. + let a = angle + while (a < TOP) a += TAU + while (a >= TOP + TAU) a -= TAU + return slices.findIndex((s) => a >= s.start && a < s.end) +} + +export type RadarAxis = { label: string; angle: number } + +/** Evenly-spaced spokes, one per data row, labelled by `nameKey`. */ +export function radarAxes(data: Row[], nameKey: string): RadarAxis[] { + const n = Math.max(data.length, 1) + return data.map((r, i) => ({ + label: String(r[nameKey] ?? i), + angle: TOP + (i / n) * TAU, + })) +} + +/** Nearest radar spoke to a pointer angle. */ +export function axisAtAngle(axes: RadarAxis[], angle: number): number { + let best = 0 + let bestD = Infinity + axes.forEach((ax, i) => { + let d = Math.abs(((angle - ax.angle + Math.PI * 3) % TAU) - Math.PI) + d = Math.abs(d) + if (d < bestD) { + bestD = d + best = i + } + }) + return best +} + +export const polarX = (cx: number, r: number, angle: number) => + cx + Math.cos(angle) * r +export const polarY = (cy: number, r: number, angle: number) => + cy + Math.sin(angle) * r + +/** Even-odd point-in-polygon test (polygon as flat [x0,y0,x1,y1,…]). */ +export function pointInPolygon( + px: number, + py: number, + poly: number[] +): boolean { + let inside = false + const n = poly.length / 2 + for (let i = 0, j = n - 1; i < n; j = i++) { + const xi = poly[i * 2] ?? 0 + const yi = poly[i * 2 + 1] ?? 0 + const xj = poly[j * 2] ?? 0 + const yj = poly[j * 2 + 1] ?? 0 + if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) { + inside = !inside + } + } + return inside +} + +/** Distance from a point to the nearest edge of a polygon — drives the radial + * dither density (dense near the edge, thinning to the centre). */ +export function distToPolygonEdge( + px: number, + py: number, + poly: number[] +): number { + let best = Infinity + const n = poly.length / 2 + for (let i = 0, j = n - 1; i < n; j = i++) { + const xi = poly[i * 2] ?? 0 + const yi = poly[i * 2 + 1] ?? 0 + const xj = poly[j * 2] ?? 0 + const yj = poly[j * 2 + 1] ?? 0 + const dx = xj - xi + const dy = yj - yi + const len2 = dx * dx + dy * dy || 1 + let t = ((px - xi) * dx + (py - yi) * dy) / len2 + t = Math.max(0, Math.min(1, t)) + const ex = xi + t * dx - px + const ey = yi + t * dy - py + const d = Math.hypot(ex, ey) + if (d < best) best = d + } + return best +} 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/reference-line.tsx b/packages/ui/src/components/dither-kit/reference-line.tsx new file mode 100644 index 000000000..76297941f --- /dev/null +++ b/packages/ui/src/components/dither-kit/reference-line.tsx @@ -0,0 +1,50 @@ +"use client" + +import { useChartPart } from "./chart-context" + +/** + * A horizontal marker line at a value on the y-axis — most useful as the zero + * baseline for diverging data (``), or to mark a target + * / threshold. Renders in the front SVG layer so it stays visible over the + * dither fill; pass an optional `label` to annotate it at the right edge. + */ +export function ReferenceLine({ + y = 0, + label, + strokeDasharray = "4 4", + className = "stroke-muted-foreground/60", +}: { + y?: number + label?: string + strokeDasharray?: string + className?: string +}) { + const ctx = useChartPart("ReferenceLine") + if (!ctx.ready) return null + + const { width } = ctx.plot + const py = ctx.y(y) + + return ( + + + {label ? ( + + {label} + + ) : null} + + ) +} diff --git a/packages/ui/src/components/dither-kit/scales.ts b/packages/ui/src/components/dither-kit/scales.ts new file mode 100644 index 000000000..ed0fd2fcf --- /dev/null +++ b/packages/ui/src/components/dither-kit/scales.ts @@ -0,0 +1,109 @@ +import { scaleBand, scaleLinear, scalePoint } from "d3-scale" +import { stack as d3Stack, stackOffsetExpand } from "d3-shape" + +export type StackType = "default" | "stacked" | "percent" + +type Row = Record + +const num = (v: unknown) => + typeof v === "number" && Number.isFinite(v) ? v : 0 + +/** + * Per-series [y0, y1] bands for every row. For `default` every series sits on + * the zero baseline (y0 = 0), so a negative value yields `[0, v]` with `v < 0` + * and draws below the baseline; for `stacked`/`percent` they pile on top of + * each other via d3's stack layout (which splits negatives below zero). The + * shape `bands[key][i] = [y0, y1]` is what both the SVG area paths and the + * canvas overlay read from. `max`/`min` bound the value range so the y-scale + * can span a diverging (below-zero) domain. + */ +export function computeBands( + data: Row[], + keys: string[], + stackType: StackType +): { bands: Record; max: number; min: number } { + if (stackType === "default") { + const bands: Record = {} + let max = 0 + let min = 0 + for (const key of keys) { + bands[key] = data.map((row) => { + const v = num(row[key]) + if (v > max) max = v + if (v < min) min = v + return [0, v] + }) + } + // Only fall back to a unit span when there's no range at all (empty / + // all-zero) — a purely negative series keeps max = 0 so the baseline + // stays pinned to the top of the plot. + const flat = max === 0 && min === 0 + return { bands, max: flat ? 1 : max, min } + } + + const series = d3Stack() + .keys(keys) + .value((row, key) => num(row[key])) + .offset(stackType === "percent" ? stackOffsetExpand : (undefined as never))( + data + ) + + const bands: Record = {} + let max = 0 + let min = 0 + series.forEach((layer) => { + bands[layer.key] = layer.map((point) => { + if (point[1] > max) max = point[1] + if (point[0] < min) min = point[0] + return [point[0], point[1]] + }) + }) + const flat = max === 0 && min === 0 + return { bands, max: flat ? 1 : max, min } +} + +/** x positions for each row index, evenly spread across the plot width. */ +export function buildXScale(length: number, plotWidth: number) { + return scalePoint() + .domain(Array.from({ length }, (_, i) => i)) + .range([0, plotWidth]) +} + +/** Banded x for bar categories — each index owns a slot of `bandwidth` width. */ +export function buildBandScale(length: number, plotWidth: number) { + return scaleBand() + .domain(Array.from({ length }, (_, i) => i)) + .range([0, plotWidth]) + .paddingInner(0.28) + .paddingOuter(0.18) +} + +/** Index of the category whose band a horizontal pixel offset falls in. */ +export function indexAtBand(px: number, length: number, plotWidth: number) { + if (length <= 0 || plotWidth <= 0) return 0 + const t = Math.max(0, Math.min(0.999, px / plotWidth)) + return Math.min(length - 1, Math.floor(t * length)) +} + +/** + * value → vertical pixel. The domain always includes zero, so charts with only + * positive values keep a floor at the plot bottom, while diverging data (values + * below zero) draws below a zero baseline that sits somewhere inside the plot. + */ +export function buildYScale(min: number, max: number, plotHeight: number) { + const lo = Math.min(0, min) + const hi = Math.max(0, max) + // Guard a degenerate (zero-width) domain so `nice()` and the range map stay + // finite even when every value is exactly zero. + return scaleLinear() + .domain([lo, hi === lo ? lo + 1 : hi]) + .nice() + .range([plotHeight, 0]) +} + +/** Index of the row nearest a horizontal pixel offset within the plot. */ +export function nearestIndex(px: number, length: number, plotWidth: number) { + if (length <= 1 || plotWidth <= 0) return 0 + const t = Math.max(0, Math.min(1, px / plotWidth)) + return Math.round(t * (length - 1)) +} diff --git a/packages/ui/src/components/dither-kit/series-context.tsx b/packages/ui/src/components/dither-kit/series-context.tsx new file mode 100644 index 000000000..35e287530 --- /dev/null +++ b/packages/ui/src/components/dither-kit/series-context.tsx @@ -0,0 +1,23 @@ +"use client" + +import { createContext, use } from "react" +import type { Seed } from "./palette" + +export type SeriesContextValue = { + dataKey: string + seed: Seed + dimmed: boolean +} + +export const SeriesContext = createContext(null) + +/** Boundary guard for series-scoped markers (``, ``). */ +export function useSeries(part: string) { + const ctx = use(SeriesContext) + if (!ctx) { + throw new Error( + `<${part} /> must be rendered inside a series (e.g. ).` + ) + } + return ctx +} diff --git a/packages/ui/src/components/dither-kit/sparkline.tsx b/packages/ui/src/components/dither-kit/sparkline.tsx new file mode 100644 index 000000000..5afe3ffc5 --- /dev/null +++ b/packages/ui/src/components/dither-kit/sparkline.tsx @@ -0,0 +1,66 @@ +"use client" + +import { useMemo } from "react" +import { Area } from "./area" +import { AreaChart } from "./area-chart" +import type { AreaVariant } from "./chart-context" +import type { BloomInput } from "./dither-paint" +import type { DitherColor } from "./palette" + +export type SparklineProps = { + /** Plain numeric series — the common sparkline case. */ + data: number[] + color: DitherColor + variant?: AreaVariant + /** Controlled crosshair position (e.g. a committed point). */ + markerIndex?: number | null + /** Parent-driven hover (e.g. the whole card/row) — lifts the fill. */ + hovered?: boolean + /** Glow on the dither fill. */ + bloom?: BloomInput + /** Only bloom while hovered. */ + bloomOnHover?: boolean + /** Play the entrance sweep — off by default for a calm spark. */ + animate?: boolean + className?: string +} + +/** + * Thin wrapper over {@link AreaChart} for the decorative-sparkline case: a + * single `number[]` series, no axes/grid/tooltip, no scrub crosshair (unless a + * `markerIndex` is supplied). Keeps the hover brightness lift. + */ +export function Sparkline({ + data, + color, + variant = "gradient", + markerIndex = null, + hovered = false, + bloom = "off", + bloomOnHover = false, + animate = false, + className, +}: SparklineProps) { + // Memoized explicitly so the chart works without React Compiler: `rows` + // identity drives the entrance-replay revision, so a fresh array every + // render would re-trigger the revision's state adjustment each pass. + const rows = useMemo(() => data.map((v) => ({ v })), [data]) + const config = useMemo(() => ({ v: { color } }), [color]) + + return ( + + + + ) +} diff --git a/packages/ui/src/components/dither-kit/tooltip.tsx b/packages/ui/src/components/dither-kit/tooltip.tsx new file mode 100644 index 000000000..2edf93e46 --- /dev/null +++ b/packages/ui/src/components/dither-kit/tooltip.tsx @@ -0,0 +1,122 @@ +"use client" + +import { AnimatePresence, motion } from "motion/react" +import { useState } from "react" +import { useCommonChart } from "./common-context" +import { cn } from "./lib" +import { rgb } from "./palette" + +export type TooltipVariant = "default" | "frosted-glass" + +const VARIANT: Record = { + default: "bg-popover", + "frosted-glass": "bg-popover/70 backdrop-blur-sm", +} + +/** + * Floating hover tooltip. Reads the shared common context so it works in every + * chart family. It glides between points and fades in/out (instead of snapping), + * and dims unselected series/slices. + */ +export function Tooltip({ + labelKey, + valueFormatter, + variant = "default", + sortItems = "desc", + inlineHeading = false, +}: { + labelKey?: string + valueFormatter?: (value: number, name: string) => string + variant?: TooltipVariant + /** 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 + + // Retain the last hovered index so the card keeps its content while fading + // out — adjust-state-during-render (no refs in render). + const [lastIndex, setLastIndex] = useState(0) + if (chart.hoverIndex != null && chart.hoverIndex !== lastIndex) { + setLastIndex(chart.hoverIndex) + } + const index = chart.hoverIndex ?? lastIndex + + const heading = chart.heading(index, labelKey) + const rawItems = chart.itemsAt(index) + const items = + sortItems === "none" + ? rawItems + : [...rawItems].sort((a, b) => + sortItems === "desc" ? b.value - a.value : a.value - b.value + ) + + return ( + + {show && items.length > 0 && ( + + {heading && !inlineHeading && ( +
+ {heading} +
+ )} +
+ {items.map((item) => ( +
+ + + {inlineHeading && heading ? heading : item.label} + + + {valueFormatter + ? valueFormatter(item.value, item.name) + : item.value.toLocaleString()} + +
+ ))} +
+
+ )} +
+ ) +} + +Tooltip.chartLayer = "dom" as const diff --git a/packages/ui/src/components/dither-kit/use-chart-dimensions.ts b/packages/ui/src/components/dither-kit/use-chart-dimensions.ts new file mode 100644 index 000000000..6a69a1fad --- /dev/null +++ b/packages/ui/src/components/dither-kit/use-chart-dimensions.ts @@ -0,0 +1,37 @@ +import { useLayoutEffect, useRef, useState } from "react" + +export type Dimensions = { width: number; height: number } + +/** + * Tracks an element's CSS pixel size via {@link ResizeObserver}. Uses + * `clientWidth`/`clientHeight` (the layout size) rather than + * `getBoundingClientRect()` so a parent `layoutId` morph — which scales the + * element via a transform — can't trick the chart into measuring a scaled size + * and locking its canvas to it. + */ +export function useChartDimensions() { + const ref = useRef(null) + const [size, setSize] = useState({ width: 0, height: 0 }) + + useLayoutEffect(() => { + const el = ref.current + if (!el) return + + const measure = () => { + const width = Math.max(0, el.clientWidth) + const height = Math.max(0, el.clientHeight) + setSize((prev) => + prev.width === width && prev.height === height + ? prev // guard against repeat fires + : { width, height } + ) + } + + const ro = new ResizeObserver(measure) + ro.observe(el) + measure() + return () => ro.disconnect() + }, []) + + return { ref, size } +} diff --git a/packages/ui/src/components/dither-kit/x-axis.tsx b/packages/ui/src/components/dither-kit/x-axis.tsx new file mode 100644 index 000000000..b9a505c8c --- /dev/null +++ b/packages/ui/src/components/dither-kit/x-axis.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useChartPart } from "./chart-context" + +export function XAxis({ + dataKey, + tickFormatter, + tickMargin = 8, + maxTicks = 8, +}: { + dataKey?: string + tickFormatter?: (value: unknown, index: number) => string + tickMargin?: number + maxTicks?: number +}) { + const ctx = useChartPart("XAxis") + if (!ctx.ready) return null + + const step = Math.max(1, Math.ceil(ctx.dataLength / maxTicks)) + const y = ctx.plot.height + tickMargin + + return ( + + {ctx.data.map((row, i) => { + if (i % step !== 0) return null + const raw = dataKey ? row[dataKey] : i + const label = tickFormatter ? tickFormatter(raw, i) : String(raw ?? "") + return ( + + {label} + + ) + })} + + ) +} diff --git a/packages/ui/src/components/dither-kit/y-axis.tsx b/packages/ui/src/components/dither-kit/y-axis.tsx new file mode 100644 index 000000000..b03db570c --- /dev/null +++ b/packages/ui/src/components/dither-kit/y-axis.tsx @@ -0,0 +1,33 @@ +"use client" + +import { useChartPart } from "./chart-context" + +export function YAxis({ + tickFormatter, + tickCount = 4, + tickMargin = 8, +}: { + tickFormatter?: (value: number) => string + tickCount?: number + tickMargin?: number +}) { + const ctx = useChartPart("YAxis") + if (!ctx.ready) return null + + return ( + + {ctx.y.ticks(tickCount).map((t) => ( + + {tickFormatter ? tickFormatter(t) : t} + + ))} + + ) +} diff --git a/turbo.json b/turbo.json index abdf95758..9b519f65f 100644 --- a/turbo.json +++ b/turbo.json @@ -44,6 +44,11 @@ "UNKEY_ROOT_KEY", "UNKEY_API_ID", "TWITTER_BEARER_TOKEN", + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "PERPLEXITY_API_KEY", + "TINYBIRD_TOKEN", + "TINYBIRD_BASE_URL", "POST_FOR_ME_API_KEY", "POST_FOR_ME_API_KEY_TWITTER", "POST_FOR_ME_API_KEY_LINKEDIN", @@ -82,7 +87,11 @@ "EVE_NOTRA_AGENT_PASSWORD", "NOTRA_AGENT_CHAT", "NOTRA_AGENT_CONTENT", - "API_VERCEL_PROJECT_NAME" + "API_VERCEL_PROJECT_NAME", + "BEACON_INGEST_SECRET", + "BEACON_INGEST_URL", + "BEACON_ORG_TOKEN", + "BEACON_ORG_ID" ], "envMode": "strict", "tasks": {