diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index 5fb7a3a8..39af6b7a 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -22,11 +22,13 @@ const config = [ // Next 16 bundles eslint-plugin-react-hooks v6 (React-Compiler-era) which // promotes these to errors. Our existing usages are intentional, benign // patterns (reset-optimistic-overlay-on-SWR-refresh effects; a monotonic - // key counter in a once-only lazy useState initializer) — keep them as - // warnings to match this repo's lenient lint posture. Revisit if we adopt - // the React Compiler. + // key counter in a once-only lazy useState initializer; Date.now() in async + // Server Components whose renders are request-scoped, not pure-functional) — + // keep them as warnings to match this repo's lenient lint posture. Revisit + // if we adopt the React Compiler. "react-hooks/set-state-in-effect": "warn", "react-hooks/refs": "warn", + "react-hooks/purity": "warn", }, }, ]; diff --git a/apps/web/src/__tests__/no-hardcoded-currency.test.ts b/apps/web/src/__tests__/no-hardcoded-currency.test.ts index ef9e4cd6..c5b56235 100644 --- a/apps/web/src/__tests__/no-hardcoded-currency.test.ts +++ b/apps/web/src/__tests__/no-hardcoded-currency.test.ts @@ -49,6 +49,25 @@ const ALLOWED = [ // static bound labels on range inputs (min/max markers), not user data. "apps/web/src/app/(dashboard)/funding/funding-details.tsx", + // ── Competitor pricing collector ───────────────────────────────────────── + // pricing.ts: /(\$|€|EUR|£|GBP)?/ and /(\$|€|£)/ regex patterns used to + // PARSE currency symbols from third-party competitor HTML pages. + // symbolToCurrency map normalises parsed glyphs to ISO codes. Not display code. + "apps/web/src/lib/competitor/collectors/pricing.ts", + + // ── Competitor collector-reframe tests: scraped-HTML fixtures ───────────── + // The Tier-1/Tier-2 tests use `$29`/`$39`/`$${amount}` INSIDE sample + // competitor HTML strings (what we fetch + normalize + diff) and as expected + // normalized-text assertions — third-party scraped-price INPUT, never app + // display code. Same rationale as the pricing.ts collector entry above. + // pipeline.test.ts (Tier-1/2 HTML fixtures), engine normalize/text-diff tests + // (normalized-text fixtures), db competitor.test.ts (snapshot raw/normalized). + "apps/web/src/lib/competitor/__tests__/", + "packages/engine/src/competitor/__tests__/", + // File-scoped (not the whole db __tests__ dir): only competitor.test.ts holds + // scraped-price fixtures; the DB layer has no display code to guard elsewhere. + "packages/db/src/__tests__/competitor.test.ts", + // ── CSV import parser ───────────────────────────────────────────────────── // import-flow.tsx line 134: /[$,€£()]/ in a regex to STRIP currency // characters from user-supplied CSV amounts. Not display code. @@ -225,6 +244,11 @@ const ALLOWED = [ "apps/web/src/app/(dashboard)/ai/_components/generative/diff-gate.tsx", "apps/web/src/__tests__/component-reachability.test.ts", "apps/web/src/__tests__/no-console-in-production.test.ts", + // feed.ts line 8: `.replace(//g, "$1")` — a String.replace() + // backreference stripping CDATA wrappers from RSS/Atom feed text. `$1` is a + // regex capture-group reference, not a currency amount. Same false-positive + // class as the camelCase-splitter entries above. + "apps/web/src/lib/competitor/collectors/feed.ts", ]; /** diff --git a/apps/web/src/__tests__/no-mutation-invalidation-leak.test.ts b/apps/web/src/__tests__/no-mutation-invalidation-leak.test.ts index 289c800d..210ce40f 100644 --- a/apps/web/src/__tests__/no-mutation-invalidation-leak.test.ts +++ b/apps/web/src/__tests__/no-mutation-invalidation-leak.test.ts @@ -37,6 +37,10 @@ const ALLOWED: { match: string; why: string }[] = [ match: "mcp/oauth/callback", why: "Completes an MCP OAuth handshake and re-probes the connection, invalidating only `mcp-connections` (non-financial). Same rationale as mcp/connections — no financial data changed.", }, + { + match: "competitors", + why: "Competitor CRUD invalidates only the non-financial `competitors` cache (competitor list UI). No financial metric depends on competitor data and there is no MutationSource for competitors, so trackDataMutation must NOT fire — bumping it would start a bogus insight-regeneration grace countdown for changes that affect no financial compute.", + }, ]; function isAllowed(rel: string): boolean { diff --git a/apps/web/src/app/(dashboard)/competitors/[id]/competitor-profile-view.tsx b/apps/web/src/app/(dashboard)/competitors/[id]/competitor-profile-view.tsx new file mode 100644 index 00000000..1e9ebe8f --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/[id]/competitor-profile-view.tsx @@ -0,0 +1,118 @@ +"use client"; + +/** + * Competitor profile + change timeline view (Task 15). Nested route under + * /competitors — mirrors the cap-table / transactions-accounts nesting + * precedent (back-link header) and the Task-14 competitors-view conventions: + * design-system components only, SSR-seeded SWR (useCompetitorChanges + * fallbackData) for a live-updating timeline, and inline token-styled status + * spans following the StatusPill precedent (no new badge component). + * + * This view is now a thin wrapper: its own back-link + name + external-URL + * header, plus the shared which renders the Sources + * (+ "Sync now"), Latest pricing, and Activity timeline. The same body is + * mounted on the dashboard card, so the analysis renders identically in both. + * + * Pricing snapshots are competitor-scraped data; prices are rendered (inside + * the shared body) via formatCurrency() from @burnless/types using the plan's + * OWN scraped currency (falling back to the company currency) — never a + * hardcoded symbol and never forcing the company currency onto a competitor's + * price. + */ + +import Link from "next/link"; +import { ArrowLeft, ExternalLink } from "lucide-react"; +import type { CompetitorChangesPayload } from "@/lib/swr"; +import { CompetitorAnalysisBody } from "../competitor-analysis-body"; + +// ── JSON-safe prop DTOs (Date → ISO string; mirrors the list-page DTO style) ── + +export interface CompetitorProfileDto { + id: string; + name: string; + url: string; +} + +export interface SourceDto { + id: string; + /** "pricing" | "social" | "page" */ + type: string; + url: string; + enabled: boolean; + lastRunAt: string | null; + lastStatus: string | null; + /** "ok" | "broken" */ + healthState: string; +} + +interface PlanDto { + name: string; + price: { amount: number | null; currency: string | null; period: string | null }; + features: string[]; +} + +export interface SnapshotDto { + id: string; + sourceId: string; + capturedAt: string; + structured: { + plans?: PlanDto[]; + items?: Record; + urls?: Record; + count?: number; + } | null; +} + +interface CompetitorProfileViewProps { + readonly competitor: CompetitorProfileDto; + readonly sources: SourceDto[]; + /** Latest snapshot per source, aligned by index with `sources`. */ + readonly latestSnapshots: (SnapshotDto | null)[]; + readonly initialChanges: CompetitorChangesPayload; +} + +// ── View ──────────────────────────────────────────────────────────────────── + +export function CompetitorProfileView({ + competitor, + sources, + latestSnapshots, + initialChanges, +}: CompetitorProfileViewProps) { + return ( +
+
+ + + Back to Competitors + +
+
+

+ {competitor.name} +

+ + {competitor.url} + + +
+
+
+ + +
+ ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/[id]/page.tsx b/apps/web/src/app/(dashboard)/competitors/[id]/page.tsx new file mode 100644 index 00000000..2e3f98be --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/[id]/page.tsx @@ -0,0 +1,122 @@ +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +import { Suspense } from "react"; +import { notFound } from "next/navigation"; +import { + getCompetitor, + listSources, + listChanges, + getLatestSnapshot, +} from "@burnless/db"; +import { getCompany } from "@/lib/data"; +import { isDomainEnabled } from "@/lib/domain-gating"; +import { SetupPrompt } from "@/components/ui/empty-state"; +import { ReportContentSkeleton } from "@/components/reports/report-skeleton"; +import type { CompetitorChangesPayload } from "@/lib/swr"; +import { + CompetitorProfileView, + type CompetitorProfileDto, + type SourceDto, + type SnapshotDto, +} from "./competitor-profile-view"; + +export default async function CompetitorPage({ + params, +}: Readonly<{ + params: Promise<{ id: string }>; +}>) { + const { id } = await params; + + const company = await getCompany(); + if (!company) return ; + + // Page-level domain gate — mirrors the requireDomainEnabled guard the REST + // routes use and the Task-14 list page. If the competitor domain is off for + // this company/deployment, the route 404s. + if (!(await isDomainEnabled("competitor", { companyId: company.id }))) { + notFound(); + } + + const competitor = await getCompetitor(id, company.id); + if (!competitor) notFound(); + + return ( + }> + + + ); +} + +async function CompetitorContent({ + companyId, + competitorId, + competitor, +}: Readonly<{ + companyId: string; + competitorId: string; + competitor: NonNullable>>; +}>) { + const [sources, changes] = await Promise.all([ + listSources(competitorId, companyId), + listChanges(companyId, { competitorId, limit: 50 }), + ]); + const snapshots = await Promise.all(sources.map((s) => getLatestSnapshot(s.id))); + + // Shape everything to JSON-safe DTOs (Date → ISO string) so the SSR seed + // matches the client SWR fetch exactly (fallbackData applies cleanly) and no + // live Date objects cross the RSC → client boundary. + const competitorDto: CompetitorProfileDto = { + id: competitor.id, + name: competitor.name, + url: competitor.url, + }; + + const sourceDtos: SourceDto[] = sources.map((s) => ({ + id: s.id, + type: s.type, + url: s.url, + enabled: s.enabled, + lastRunAt: s.lastRunAt ? s.lastRunAt.toISOString() : null, + lastStatus: s.lastStatus, + healthState: s.healthState, + })); + + const latestSnapshots: (SnapshotDto | null)[] = snapshots.map((snap) => + snap + ? { + id: snap.id, + sourceId: snap.sourceId, + capturedAt: snap.capturedAt.toISOString(), + structured: snap.structured as SnapshotDto["structured"], + } + : null, + ); + + const initialChanges: CompetitorChangesPayload = { + changes: changes.map((c) => ({ + id: c.id, + competitorId: c.competitorId, + sourceId: c.sourceId, + snapshotId: c.snapshotId, + companyId: c.companyId, + detectedAt: c.detectedAt.toISOString(), + changeType: c.changeType, + summary: c.summary, + before: c.before as Record | null, + after: c.after as Record | null, + severity: c.severity, + acknowledgedAt: c.acknowledgedAt ? c.acknowledgedAt.toISOString() : null, + createdAt: c.createdAt.toISOString(), + })), + }; + + return ( + + ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/__tests__/competitor-analysis-body.test.tsx b/apps/web/src/app/(dashboard)/competitors/__tests__/competitor-analysis-body.test.tsx new file mode 100644 index 00000000..df0cddd5 --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/__tests__/competitor-analysis-body.test.tsx @@ -0,0 +1,59 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { CompetitorAnalysisBody } from "../competitor-analysis-body"; + +// WidgetCard (design-system) calls useRouter — mount a stub app router. +vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) })); + +// happy-dom + SWR fallbackData → no fetch needed. +const base = { + competitor: { id: "c1", name: "Acme", url: "https://acme.com" }, + sources: [ + { id: "s1", type: "page", url: "https://acme.com/x", enabled: true, lastRunAt: null, lastStatus: null, healthState: "ok" }, + ], + latestSnapshots: [null], +}; + +describe("CompetitorAnalysisBody", () => { + it("renders a content_changed row with its summary and an expandable snippet", () => { + const initialChanges = { + changes: [ + { + id: "ch1", competitorId: "c1", sourceId: "s1", snapshotId: "sn1", companyId: "co1", + detectedAt: new Date("2026-07-01").toISOString(), + changeType: "content_changed", + summary: "Page content changed: +1 / -1 lines", + before: { lines: ["Old line"], truncated: false }, + after: { lines: ["New line"], truncated: false }, + severity: "info", acknowledgedAt: null, createdAt: new Date("2026-07-01").toISOString(), + }, + ], + }; + render(); + expect(screen.getByText(/Page content changed/)).toBeTruthy(); + // Disclosure content present (details/summary): added + removed lines. + // The snippet renders each line with a diff prefix ("+ New line" / "- Old + // line"), so match with a regex rather than an exact string. + expect(screen.getByText(/New line/)).toBeTruthy(); + expect(screen.getByText(/Old line/)).toBeTruthy(); + }); + + it("renders Recent posts from a feed snapshot and Pages-tracked from a sitemap snapshot", () => { + const props = { + competitor: { id: "c1", name: "Acme", url: "https://acme.com" }, + sources: [ + { id: "f1", type: "feed", url: "https://acme.com/rss", enabled: true, lastRunAt: null, lastStatus: null, healthState: "ok" }, + { id: "m1", type: "sitemap", url: "https://acme.com/sitemap.xml", enabled: true, lastRunAt: null, lastStatus: null, healthState: "ok" }, + ], + latestSnapshots: [ + { id: "sn1", sourceId: "f1", capturedAt: new Date("2026-07-01").toISOString(), structured: { items: { g1: { title: "Launch Day", link: "https://acme.com/p/1", publishedAt: new Date("2026-07-01").toISOString() } } } }, + { id: "sn2", sourceId: "m1", capturedAt: new Date("2026-07-01").toISOString(), structured: { urls: { "https://acme.com/": 1, "https://acme.com/pricing": 1 }, count: 2 } }, + ], + initialChanges: { changes: [] }, + }; + render(); + expect(screen.getByText("Launch Day")).toBeTruthy(); + expect(screen.getByText(/Pages tracked/i)).toBeTruthy(); + expect(screen.getByText("2")).toBeTruthy(); + }); +}); diff --git a/apps/web/src/app/(dashboard)/competitors/competitor-analysis-body.tsx b/apps/web/src/app/(dashboard)/competitors/competitor-analysis-body.tsx new file mode 100644 index 00000000..183a8f72 --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/competitor-analysis-body.tsx @@ -0,0 +1,284 @@ +"use client"; + +/** + * Shared competitor analysis body (Tiers-1+2 reframe): Sources (+ "Sync now"), + * Latest pricing, and the Activity timeline. Mounted in two places — the + * /competitors/[id] permalink (with a header) and the dashboard card (expanded) + * — so the analysis renders identically in both. Design-system components only; + * a `content_changed` change gets an inline
disclosure (existing + * tokens, no new component) showing the added/removed line snippet. + */ + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { ExternalLink, RefreshCw } from "lucide-react"; +import { DataTable, Button, WidgetCard } from "@/components/ui"; +import { useLocale } from "@/components/locale/locale-context"; +import { formatCurrency, isValidCurrency, type CurrencyCode } from "@burnless/types"; +import { apiFetch } from "@/lib/api-fetch"; +import { toUserMessage } from "@/lib/api-error"; +import { + useCompetitorChanges, + type CompetitorChangeDto, + type CompetitorChangesPayload, +} from "@/lib/swr"; +import type { + CompetitorProfileDto, + SourceDto, + SnapshotDto, +} from "./[id]/competitor-profile-view"; + +const PILL_BASE = + "inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium uppercase"; + +function HealthBadge({ healthState, lastStatus }: Readonly<{ healthState: string; lastStatus: string | null }>) { + if (healthState === "broken") { + return ( + + Needs attention + + ); + } + return Healthy; +} + +function SeverityBadge({ severity }: Readonly<{ severity: string }>) { + const nonCriticalCls = severity === "warning" ? "bg-warning-50 text-warning-700" : "bg-surface-100 text-surface-600"; + const cls = severity === "critical" ? "bg-danger-50 text-danger-600" : nonCriticalCls; + return {severity}; +} + +/** Content-change line snippet (added green / removed red) — reuse tokens only. */ +function ContentSnippet({ change }: Readonly<{ change: CompetitorChangeDto }>) { + const before = (change.before as { lines?: string[]; truncated?: boolean } | null) ?? null; + const after = (change.after as { lines?: string[]; truncated?: boolean } | null) ?? null; + const removed = before?.lines ?? []; + const added = after?.lines ?? []; + if (removed.length === 0 && added.length === 0) return null; + return ( +
+ + View changed lines + +
+ {removed.map((l, i) => ( +
- {l}
+ ))} + {added.map((l, i) => ( +
+ {l}
+ ))} + {(before?.truncated || after?.truncated) && ( +
… more lines truncated
+ )} +
+
+ ); +} + +interface Props { + readonly competitor: CompetitorProfileDto; + readonly sources: SourceDto[]; + readonly latestSnapshots: (SnapshotDto | null)[]; + readonly initialChanges: CompetitorChangesPayload; +} + +export function CompetitorAnalysisBody({ competitor, sources, latestSnapshots, initialChanges }: Props) { + const router = useRouter(); + const { fmtDate, currency, locale } = useLocale(); + const { data, mutate } = useCompetitorChanges(competitor.id, { fallbackData: initialChanges }); + const [syncing, setSyncing] = useState(false); + const [syncError, setSyncError] = useState(null); + + const changes = data?.changes ?? initialChanges.changes; + + const pricingPlans = (() => { + for (let i = 0; i < sources.length; i++) { + if (sources[i]?.type !== "pricing") continue; + const plans = latestSnapshots[i]?.structured?.plans; + if (plans && plans.length > 0) return plans; + } + return null; + })(); + + const feedPosts = (() => { + for (let i = 0; i < sources.length; i++) { + if (sources[i]?.type !== "feed") continue; + const items = latestSnapshots[i]?.structured?.items; + if (items && Object.keys(items).length > 0) { + return Object.values(items) + .sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? "")) + .slice(0, 10); + } + } + return null; + })(); + + const siteCount = (() => { + for (let i = 0; i < sources.length; i++) { + if (sources[i]?.type !== "sitemap") continue; + const c = latestSnapshots[i]?.structured?.count; + if (typeof c === "number") return c; + } + return null; + })(); + + async function handleSync() { + setSyncError(null); + setSyncing(true); + try { + const res = await apiFetch(`/api/competitors/${competitor.id}/sync`, { method: "POST" }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? "Failed to sync"); + } + // mutate() refreshes the SWR changes timeline; router.refresh() re-runs + // the RSC props (source health / last-checked / latest-pricing snapshot), + // which don't come from SWR — otherwise "Sync now" leaves them stale + // until a manual reload. Correct for both mounts (permalink + dashboard). + mutate(); + router.refresh(); + } catch (err) { + setSyncError(toUserMessage(err)); + } finally { + setSyncing(false); + } + } + + const planColumns = [ + { key: "name", header: "Plan", render: (p: { name: string }) => {p.name} }, + { + key: "price", + header: "Price", + align: "right" as const, + render: (p: { price: { amount: number | null; currency: string | null; period: string | null } }) => { + const planCurrency = isValidCurrency(p.price.currency ?? "") ? (p.price.currency as CurrencyCode) : currency; + return ( + + {p.price.amount != null + ? formatCurrency(p.price.amount, planCurrency, locale) + : "—"} + {p.price.period ? {` / ${p.price.period}`} : null} + + ); + }, + }, + ]; + + const changeColumns = [ + { key: "severity", header: "Severity", render: (c: CompetitorChangeDto) => , sortValue: (c: CompetitorChangeDto) => c.severity }, + { + key: "summary", + header: "Change", + render: (c: CompetitorChangeDto) => ( +
+ {c.summary} + {c.changeType === "content_changed" && } +
+ ), + }, + { key: "detectedAt", header: "Detected", align: "right" as const, render: (c: CompetitorChangeDto) => {fmtDate(c.detectedAt)}, sortValue: (c: CompetitorChangeDto) => c.detectedAt }, + ]; + + return ( +
+ {syncError && ( +
+ {syncError} +
+ )} + +
+

Sources

+ {sources.length === 0 ? ( +

No sources tracked for this competitor.

+ ) : ( +
+ {sources.map((source) => ( + +
+
+
+ {source.type} + +
+ + {source.url} + + +

+ {source.lastRunAt ? `Last checked ${fmtDate(source.lastRunAt)}` : "Never checked"} +

+
+ +
+
+ ))} +
+ )} +
+ + {pricingPlans && ( +
+

Latest pricing

+
+ p.name} emptyMessage="No pricing plans captured." /> +
+
+ )} + + {feedPosts && ( +
+

Recent posts

+
+ + p.link ? ( + + {p.title} + + ) : ( + {p.title} + ), + }, + { + key: "publishedAt", + header: "Published", + align: "right" as const, + render: (p: { publishedAt: string | null }) => ( + {p.publishedAt ? fmtDate(p.publishedAt) : "—"} + ), + }, + ]} + data={feedPosts} + rowKey={(p) => p.link || p.title} + emptyMessage="No posts captured." + /> +
+
+ )} + + {siteCount != null && ( +
+

Site structure

+ +

Pages tracked

+

{siteCount}

+
+
+ )} + +
+

Activity

+
+ c.id} emptyMessage="No changes detected yet." /> +
+
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/competitor-card.tsx b/apps/web/src/app/(dashboard)/competitors/competitor-card.tsx new file mode 100644 index 00000000..2ca62aa8 --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/competitor-card.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { ChevronDown, ChevronRight, ExternalLink } from "lucide-react"; +import { WidgetCard } from "@/components/ui"; +import { useLocale } from "@/components/locale/locale-context"; +import { CompetitorAnalysisBody } from "./competitor-analysis-body"; +import type { CompetitorProfileDto, SourceDto, SnapshotDto } from "./[id]/competitor-profile-view"; +import type { CompetitorChangesPayload, CompetitorChangeDto } from "@/lib/swr"; + +export interface CompetitorCardData { + competitor: CompetitorProfileDto; + sources: SourceDto[]; + latestSnapshots: (SnapshotDto | null)[]; + initialChanges: CompetitorChangesPayload; + changeCount30d: number; + latestChange: CompetitorChangeDto | null; +} + +export function CompetitorCard({ data }: Readonly<{ data: CompetitorCardData }>) { + const { fmtDate } = useLocale(); + const [open, setOpen] = useState(false); + const { competitor, changeCount30d, latestChange } = data; + + return ( + +
+ + + Open + +
+ + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/competitors-dashboard.tsx b/apps/web/src/app/(dashboard)/competitors/competitors-dashboard.tsx new file mode 100644 index 00000000..f5298ef4 --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/competitors-dashboard.tsx @@ -0,0 +1,48 @@ +"use client"; + +/** + * Competitor analysis dashboard (reframe): one expandable card per competitor, + * an at-a-glance recent-change-activity headline collapsed, full analysis on + * expand. Management (CRUD) lives at /competitors/manage — mirrors the + * transactions → accounts nesting. Design-system components + tokens only. + */ + +import Link from "next/link"; +import { Swords, Settings2 } from "lucide-react"; +import { Button, DataEmptyState } from "@/components/ui"; +import { CompetitorCard, type CompetitorCardData } from "./competitor-card"; + +export type { CompetitorCardData }; + +export function CompetitorsDashboard({ cards }: Readonly<{ cards: CompetitorCardData[] }>) { + return ( +
+
+
+

Competitors

+

+ Continuous analysis of each competitor — pricing, social, and page changes +

+
+ + + +
+ + {cards.length === 0 ? ( + } + /> + ) : ( +
+ {cards.map((card) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/manage/competitors-view.tsx b/apps/web/src/app/(dashboard)/competitors/manage/competitors-view.tsx new file mode 100644 index 00000000..e8cfcaec --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/manage/competitors-view.tsx @@ -0,0 +1,345 @@ +"use client"; + +/** + * Competitors browse + manage view (Task 14). Lightweight DataTable (not the + * metric grid), mirroring the WS2 transactions-view pattern: SSR-seeded SWR + * (useCompetitors fallbackData) that live-updates via the competitor mutation + * bus, an "Add competitor" Modal, and useConfirm-gated delete. No currency on + * this surface. + */ + +import { useState } from "react"; +import Link from "next/link"; +import { Swords, Trash2, ExternalLink, ArrowLeft, Rss } from "lucide-react"; +import { + DataTable, + Button, + Modal, + Input, + DataEmptyState, + useConfirm, +} from "@/components/ui"; +import { apiFetch } from "@/lib/api-fetch"; +import { toUserMessage } from "@/lib/api-error"; +import { useCompetitors, type CompetitorDto, type CompetitorsPayload } from "@/lib/swr"; + +interface CompetitorsViewProps { + readonly initialData: CompetitorsPayload; +} + +/** Reuse-only status pill — no @/components/ui badge component exists, so a + * token-styled (matches the transactions SourcePill precedent). NOT a + * new component. */ +function StatusPill({ status }: Readonly<{ status: string }>) { + const cls = + status === "active" + ? "bg-success-50 text-success-700" + : "bg-surface-100 text-surface-600"; + return ( + + {status} + + ); +} + +export function CompetitorsView({ initialData }: CompetitorsViewProps) { + const { data, mutate } = useCompetitors({ fallbackData: initialData }); + const { confirm, dialog } = useConfirm(); + + const [adding, setAdding] = useState(false); + const [actionError, setActionError] = useState(null); + + const rows = data?.competitors ?? []; + + async function handleDelete(row: CompetitorDto) { + const ok = await confirm({ + title: "Delete competitor", + body: `Delete "${row.name}"? This removes the competitor and its tracked sources and changes.`, + confirmLabel: "Delete", + destructive: true, + }); + if (!ok) return; + setActionError(null); + try { + const res = await apiFetch(`/api/competitors/${row.id}`, { method: "DELETE" }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? "Failed to delete competitor"); + } + mutate(); + } catch (err) { + setActionError(toUserMessage(err)); + } + } + + async function handleDetect(row: CompetitorDto) { + setActionError(null); + try { + const res = await apiFetch(`/api/competitors/${row.id}/detect`, { method: "POST" }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? "Failed to detect feeds"); + } + mutate(); + } catch (err) { + setActionError(toUserMessage(err)); + } + } + + const columns = [ + { + key: "name", + header: "Name", + render: (r: CompetitorDto) => ( + + {r.name} + + ), + sortValue: (r: CompetitorDto) => r.name.toLowerCase(), + }, + { + key: "url", + header: "URL", + render: (r: CompetitorDto) => ( + e.stopPropagation()} + > + {r.url} + + + ), + }, + { + key: "status", + header: "Status", + render: (r: CompetitorDto) => , + sortValue: (r: CompetitorDto) => r.status, + }, + { + key: "actions", + header: "", + align: "right" as const, + render: (r: CompetitorDto) => ( +
+ + +
+ ), + }, + ]; + + return ( +
+
+
+ + + Back to Competitors + +

Manage competitors

+

+ Add competitors and configure the pages we watch for changes +

+
+
+ +
+
+ + {actionError && ( +
+ {actionError} +
+ )} + + {rows.length === 0 ? ( + setAdding(true)}>Add competitor} + /> + ) : ( +
+ r.id} + emptyMessage="No competitors yet. Add one to start tracking." + /> +
+ )} + + {adding && ( + setAdding(false)} + onAdded={() => { + setAdding(false); + mutate(); + }} + /> + )} + + {dialog} +
+ ); +} + +// ── Add competitor modal ─────────────────────────────────────────────────────── + +function AddCompetitorModal({ + open, + onClose, + onAdded, +}: Readonly<{ + open: boolean; + onClose: () => void; + onAdded: () => void; +}>) { + const [name, setName] = useState(""); + const [url, setUrl] = useState(""); + const [pricingUrl, setPricingUrl] = useState(""); + const [socialUrl, setSocialUrl] = useState(""); + const [pageUrl, setPageUrl] = useState(""); + const [feedUrl, setFeedUrl] = useState(""); + const [sitemapUrl, setSitemapUrl] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setSubmitting(true); + try { + const sources = [ + ...(pricingUrl ? [{ type: "pricing" as const, url: pricingUrl }] : []), + ...(socialUrl ? [{ type: "social" as const, url: socialUrl }] : []), + ...(pageUrl ? [{ type: "page" as const, url: pageUrl }] : []), + ...(feedUrl ? [{ type: "feed" as const, url: feedUrl }] : []), + ...(sitemapUrl ? [{ type: "sitemap" as const, url: sitemapUrl }] : []), + ]; + const res = await apiFetch("/api/competitors", { + method: "POST", + body: JSON.stringify({ name, url, sources }), + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? "Failed to add competitor"); + } + onAdded(); + } catch (err) { + setError(toUserMessage(err)); + } finally { + setSubmitting(false); + } + } + + return ( + +
+ setName(e.target.value)} + placeholder="Acme Inc." + autoFocus + /> + setUrl(e.target.value)} + placeholder="https://acme.com" + /> + setPricingUrl(e.target.value)} + placeholder="https://acme.com/pricing" + hint="We'll watch this page for pricing changes." + /> + setSocialUrl(e.target.value)} + placeholder="https://x.com/acme" + /> + setPageUrl(e.target.value)} + placeholder="https://acme.com/changelog" + hint="Any page (changelog, careers, TOS) — we'll alert you when its content changes." + /> + setFeedUrl(e.target.value)} + placeholder="https://acme.com/blog/rss" + hint="Blog / news / changelog feed — we track new posts. (We also auto-detect this.)" + /> + setSitemapUrl(e.target.value)} + placeholder="https://acme.com/sitemap.xml" + hint="We track pages appearing and disappearing." + /> + + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+ ); +} diff --git a/apps/web/src/app/(dashboard)/competitors/manage/page.tsx b/apps/web/src/app/(dashboard)/competitors/manage/page.tsx new file mode 100644 index 00000000..a3b1a6ad --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/manage/page.tsx @@ -0,0 +1,34 @@ +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +import { Suspense } from "react"; +import { notFound } from "next/navigation"; +import { listCompetitors } from "@burnless/db"; +import { getCompany } from "@/lib/data"; +import { isDomainEnabled } from "@/lib/domain-gating"; +import { SetupPrompt } from "@/components/ui/empty-state"; +import { ReportContentSkeleton } from "@/components/reports/report-skeleton"; +import { CompetitorsView } from "./competitors-view"; +import type { CompetitorsPayload } from "@/lib/swr"; + +export default async function ManageCompetitorsPage() { + const company = await getCompany(); + if (!company) return ; + if (!(await isDomainEnabled("competitor", { companyId: company.id }))) notFound(); + return ( + }> + + + ); +} + +async function ManageContent({ companyId }: Readonly<{ companyId: string }>) { + const competitors = await listCompetitors(companyId); + const initialData: CompetitorsPayload = { + competitors: competitors.map((c) => ({ + id: c.id, companyId: c.companyId, name: c.name, url: c.url, status: c.status, + createdAt: c.createdAt.toISOString(), updatedAt: c.updatedAt.toISOString(), + })), + }; + return ; +} diff --git a/apps/web/src/app/(dashboard)/competitors/page.tsx b/apps/web/src/app/(dashboard)/competitors/page.tsx new file mode 100644 index 00000000..1a086dd8 --- /dev/null +++ b/apps/web/src/app/(dashboard)/competitors/page.tsx @@ -0,0 +1,66 @@ +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +import { Suspense } from "react"; +import { notFound } from "next/navigation"; +import { listCompetitors, listSources, listChanges, getLatestSnapshot } from "@burnless/db"; +import { getCompany } from "@/lib/data"; +import { isDomainEnabled } from "@/lib/domain-gating"; +import { SetupPrompt } from "@/components/ui/empty-state"; +import { ReportContentSkeleton } from "@/components/reports/report-skeleton"; +import { CompetitorsDashboard, type CompetitorCardData } from "./competitors-dashboard"; +import type { CompetitorChangesPayload } from "@/lib/swr"; +import type { CompetitorProfileDto, SourceDto, SnapshotDto } from "./[id]/competitor-profile-view"; + +const THIRTY_DAYS_MS = 30 * 24 * 3_600_000; + +export default async function CompetitorsPage() { + const company = await getCompany(); + if (!company) return ; + if (!(await isDomainEnabled("competitor", { companyId: company.id }))) notFound(); + return ( + }> + + + ); +} + +async function DashboardContent({ companyId }: Readonly<{ companyId: string }>) { + const competitors = await listCompetitors(companyId); + const cutoff = Date.now() - THIRTY_DAYS_MS; + + const cards: CompetitorCardData[] = await Promise.all( + competitors.map(async (c) => { + const [sources, changesRaw] = await Promise.all([ + listSources(c.id, companyId), + listChanges(companyId, { competitorId: c.id, limit: 50 }), + ]); + const snapshots = await Promise.all(sources.map((s) => getLatestSnapshot(s.id))); + + const competitor: CompetitorProfileDto = { id: c.id, name: c.name, url: c.url }; + const sourceDtos: SourceDto[] = sources.map((s) => ({ + id: s.id, type: s.type, url: s.url, enabled: s.enabled, + lastRunAt: s.lastRunAt ? s.lastRunAt.toISOString() : null, + lastStatus: s.lastStatus, healthState: s.healthState, + })); + const latestSnapshots: (SnapshotDto | null)[] = snapshots.map((snap) => + snap ? { id: snap.id, sourceId: snap.sourceId, capturedAt: snap.capturedAt.toISOString(), structured: snap.structured as SnapshotDto["structured"] } : null, + ); + const initialChanges: CompetitorChangesPayload = { + changes: changesRaw.map((ch) => ({ + id: ch.id, competitorId: ch.competitorId, sourceId: ch.sourceId, snapshotId: ch.snapshotId, companyId: ch.companyId, + detectedAt: ch.detectedAt.toISOString(), changeType: ch.changeType, summary: ch.summary, + before: ch.before as Record | null, after: ch.after as Record | null, + severity: ch.severity, acknowledgedAt: ch.acknowledgedAt ? ch.acknowledgedAt.toISOString() : null, + createdAt: ch.createdAt.toISOString(), + })), + }; + const changeCount30d = changesRaw.filter((ch) => ch.detectedAt.getTime() >= cutoff).length; + const latestChange = initialChanges.changes[0] ?? null; + + return { competitor, sources: sourceDtos, latestSnapshots, initialChanges, changeCount30d, latestChange }; + }), + ); + + return ; +} diff --git a/apps/web/src/app/(dashboard)/dashboard-shell/nav-config.ts b/apps/web/src/app/(dashboard)/dashboard-shell/nav-config.ts index e412ac12..3b8d7f7e 100644 --- a/apps/web/src/app/(dashboard)/dashboard-shell/nav-config.ts +++ b/apps/web/src/app/(dashboard)/dashboard-shell/nav-config.ts @@ -10,6 +10,7 @@ import { FolderOpen, Plug, Clock, + Swords, type LucideIcon, } from "lucide-react"; @@ -40,6 +41,8 @@ export const coreNavItems: NavItem[] = [ { id: "data-room", href: "/data-room", label: "Data Room", icon: FolderOpen }, { id: "connections", href: "/connections", label: "Connections", icon: Plug }, { id: "automations", href: "/automations", label: "Automations", icon: Clock }, + // competitor domain (Task 10) — one acknowledged base-touch per A3 breadcrumb + { id: "competitors", href: "/competitors", label: "Competitors", icon: Swords }, ]; export const aiNavItem: NavItem = { id: "ai", href: "/ai", label: "Companion", icon: Sparkles }; diff --git a/apps/web/src/app/api/competitors/[id]/changes/[cid]/ack/route.ts b/apps/web/src/app/api/competitors/[id]/changes/[cid]/ack/route.ts new file mode 100644 index 00000000..deb70f6a --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/changes/[cid]/ack/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { ackChange } from "@burnless/db"; +import { requireCompanyWrite, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; + +export const POST = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string; cid: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { cid } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + await ackChange(cid, ctx.companyId, new Date()); + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ ok: true }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/changes/route.ts b/apps/web/src/app/api/competitors/[id]/changes/route.ts new file mode 100644 index 00000000..76ba83a0 --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/changes/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { listChanges } from "@burnless/db"; +import { requireCompanyAccess, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; + +export const GET = withErrorHandler(async ( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyAccess(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const url = new URL(request.url); + const limitParam = url.searchParams.get("limit"); + const parsed = Number.parseInt(limitParam ?? "", 10); + const limit = Number.isFinite(parsed) ? Math.max(1, parsed) : 50; + + const changes = await listChanges(ctx.companyId, { competitorId: id, limit }); + return NextResponse.json({ changes }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/detect/route.ts b/apps/web/src/app/api/competitors/[id]/detect/route.ts new file mode 100644 index 00000000..f77e2b89 --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/detect/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { getCompetitor, listSources, createSource } from "@burnless/db"; +import { requireCompanyWrite, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; +import { discoverFeeds } from "@/lib/competitor/discover"; + +export const POST = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const competitor = await getCompetitor(id, ctx.companyId); + if (!competitor) return NextResponse.json({ error: "Not found" }, { status: 404 }); + + const found = await discoverFeeds(competitor.url); + const existing = new Set( + (await listSources(id, ctx.companyId)).map((s) => `${s.type}:${s.url}`), + ); + let created = 0; + for (const f of found) { + if (existing.has(`${f.type}:${f.url}`)) continue; + existing.add(`${f.type}:${f.url}`); + await createSource({ + companyId: ctx.companyId, + competitorId: id, + type: f.type, + url: f.url, + }); + created++; + } + + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ created }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/route.ts b/apps/web/src/app/api/competitors/[id]/route.ts new file mode 100644 index 00000000..8a0b5be2 --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { z } from "zod"; +import { getCompetitor, updateCompetitor, deleteCompetitor } from "@burnless/db"; +import { requireCompanyAccess, requireCompanyWrite, parseBody, errorResponse, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; + +const updateCompetitorSchema = z.object({ + name: z.string().min(1).optional(), + url: z.string().url().optional(), + status: z.enum(["active", "paused"]).optional(), +}); + +export const GET = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyAccess(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const competitor = await getCompetitor(id, ctx.companyId); + if (!competitor) return errorResponse("Competitor not found", 404); + return NextResponse.json({ competitor }); +}); + +export const PATCH = withErrorHandler(async ( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const parsed = await parseBody(request, updateCompetitorSchema); + if ("error" in parsed) return parsed.error; + + const competitor = await updateCompetitor(id, ctx.companyId, parsed.data); + if (!competitor) return errorResponse("Competitor not found", 404); + + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ competitor }); +}); + +export const DELETE = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const existing = await getCompetitor(id, ctx.companyId); + if (!existing) return errorResponse("Competitor not found", 404); + + await deleteCompetitor(id, ctx.companyId); + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ ok: true }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/sources/[sid]/route.ts b/apps/web/src/app/api/competitors/[id]/sources/[sid]/route.ts new file mode 100644 index 00000000..c15d3fff --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/sources/[sid]/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { z } from "zod"; +import { updateSource, deleteSource } from "@burnless/db"; +import { requireCompanyWrite, parseBody, errorResponse, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; + +const updateSourceSchema = z.object({ + url: z.string().url().optional(), + config: z.unknown().optional(), + enabled: z.boolean().optional(), + intervalHours: z.number().int().positive().optional(), +}); + +export const PATCH = withErrorHandler(async ( + request: Request, + { params }: { params: Promise<{ id: string; sid: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { sid } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const parsed = await parseBody(request, updateSourceSchema); + if ("error" in parsed) return parsed.error; + + const source = await updateSource(sid, ctx.companyId, parsed.data); + if (!source) return errorResponse("Source not found", 404); + + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ source }); +}); + +export const DELETE = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string; sid: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { sid } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + await deleteSource(sid, ctx.companyId); + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ ok: true }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/sources/route.ts b/apps/web/src/app/api/competitors/[id]/sources/route.ts new file mode 100644 index 00000000..91d8997d --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/sources/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { z } from "zod"; +import { listSources, createSource } from "@burnless/db"; +import { requireCompanyAccess, requireCompanyWrite, parseBody, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; + +const createSourceSchema = z.object({ + type: z.enum(["pricing", "social", "page", "feed", "sitemap"]), + url: z.string().url(), + config: z.unknown().optional(), + intervalHours: z.number().int().positive().optional(), +}); + +export const GET = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyAccess(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const sources = await listSources(id, ctx.companyId); + return NextResponse.json({ sources }); +}); + +export const POST = withErrorHandler(async ( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const parsed = await parseBody(request, createSourceSchema); + if ("error" in parsed) return parsed.error; + + const { type, url, config, intervalHours } = parsed.data; + const source = await createSource({ + companyId: ctx.companyId, + competitorId: id, + type, + url, + config, + intervalHours, + }); + + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ source }, { status: 201 }); +}); diff --git a/apps/web/src/app/api/competitors/[id]/sync/route.ts b/apps/web/src/app/api/competitors/[id]/sync/route.ts new file mode 100644 index 00000000..2e0f3b11 --- /dev/null +++ b/apps/web/src/app/api/competitors/[id]/sync/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { listSources } from "@burnless/db"; +import { requireCompanyWrite, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; +import { runSource } from "@/lib/competitor/pipeline"; + +export const POST = withErrorHandler(async ( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + const { id } = await params; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const sources = await listSources(id, ctx.companyId); + const enabled = sources.filter((s) => s.enabled); + + let changed = 0; + for (const s of enabled) { + const r = await runSource(s); + if (r.changed) changed++; + } + + return NextResponse.json({ ran: enabled.length, changed }); +}); diff --git a/apps/web/src/app/api/competitors/__tests__/competitors.test.ts b/apps/web/src/app/api/competitors/__tests__/competitors.test.ts new file mode 100644 index 00000000..1b16114f --- /dev/null +++ b/apps/web/src/app/api/competitors/__tests__/competitors.test.ts @@ -0,0 +1,422 @@ +/** + * Tests for GET /api/competitors and POST /api/competitors. + * Highest-value assertion: domain gate returns 403 DOMAIN_DISABLED. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextResponse } from "next/server"; + +const { mockRequireCompanyAccess, mockRequireWrite } = vi.hoisted(() => ({ + mockRequireCompanyAccess: vi.fn(), + mockRequireWrite: vi.fn(), +})); + +const { mockRequireDomainEnabled } = vi.hoisted(() => ({ + mockRequireDomainEnabled: vi.fn(), +})); + +const { mockListCompetitors, mockCreateCompetitor, mockCreateSource, mockGetCompetitor, mockListSources } = + vi.hoisted(() => ({ + mockListCompetitors: vi.fn(), + mockCreateCompetitor: vi.fn(), + mockCreateSource: vi.fn(), + mockGetCompetitor: vi.fn(), + mockListSources: vi.fn(), + })); + +const { mockDiscoverFeeds } = vi.hoisted(() => ({ mockDiscoverFeeds: vi.fn() })); + +vi.mock("@/lib/api-helpers", () => ({ + requireCompanyAccess: mockRequireCompanyAccess, + requireCompanyWrite: mockRequireWrite, + parseBody: async (req: Request, schema: { parse: (d: unknown) => unknown }) => { + try { + return { data: schema.parse(await req.json()) }; + } catch { + return { error: NextResponse.json({ error: "Validation failed" }, { status: 400 }) }; + } + }, + errorResponse: (msg: string, status: number) => NextResponse.json({ error: msg }, { status }), + withErrorHandler: (fn: (...args: unknown[]) => unknown) => fn, +})); + +vi.mock("@/lib/domain-gating", () => ({ + requireDomainEnabled: mockRequireDomainEnabled, +})); + +vi.mock("@burnless/db", () => ({ + listCompetitors: mockListCompetitors, + createCompetitor: mockCreateCompetitor, + createSource: mockCreateSource, + getCompetitor: mockGetCompetitor, + listSources: mockListSources, +})); + +vi.mock("@/lib/competitor/discover", () => ({ discoverFeeds: mockDiscoverFeeds })); + +vi.mock("next/cache", () => ({ revalidateTag: vi.fn() })); + +import { GET, POST } from "../route"; +import { POST as DETECT } from "../[id]/detect/route"; + +const validCtx = { userId: "user-1", companyId: "company-1", role: "editor" } as const; + +function makeRequest(url: string, options?: RequestInit): Request { + return new Request(url, options); +} + +describe("GET /api/competitors", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 401 when unauthenticated", async () => { + mockRequireCompanyAccess.mockResolvedValue({ + error: NextResponse.json({ error: "Unauthorized" }, { status: 401 }), + }); + + const res = await GET(makeRequest("http://localhost/api/competitors")); + const body = await res.json(); + + expect(res.status).toBe(401); + expect(body.error).toBe("Unauthorized"); + }); + + it("returns 403 DOMAIN_DISABLED when competitor domain is off", async () => { + mockRequireCompanyAccess.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue( + NextResponse.json( + { error: "This domain is not available on this deployment", code: "DOMAIN_DISABLED", domainId: "competitor" }, + { status: 403 }, + ), + ); + + const res = await GET(makeRequest("http://localhost/api/competitors")); + const body = await res.json(); + + expect(res.status).toBe(403); + expect(body.code).toBe("DOMAIN_DISABLED"); + expect(mockListCompetitors).not.toHaveBeenCalled(); + }); + + it("returns competitors list when domain is enabled", async () => { + mockRequireCompanyAccess.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockListCompetitors.mockResolvedValue([ + { id: "c-1", name: "Acme", url: "https://acme.com", status: "active" }, + ]); + + const res = await GET(makeRequest("http://localhost/api/competitors")); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.competitors).toHaveLength(1); + expect(body.competitors[0].name).toBe("Acme"); + expect(mockListCompetitors).toHaveBeenCalledWith("company-1"); + }); +}); + +describe("POST /api/competitors", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Best-effort discovery on add defaults to finding nothing unless a test opts in. + mockDiscoverFeeds.mockResolvedValue([]); + }); + + it("returns 403 DOMAIN_DISABLED when competitor domain is off", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue( + NextResponse.json( + { error: "This domain is not available on this deployment", code: "DOMAIN_DISABLED", domainId: "competitor" }, + { status: 403 }, + ), + ); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Rival", url: "https://rival.com" }), + }), + ); + const body = await res.json(); + + expect(res.status).toBe(403); + expect(body.code).toBe("DOMAIN_DISABLED"); + expect(mockCreateCompetitor).not.toHaveBeenCalled(); + }); + + it("creates competitor and returns 201", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + status: "active", + }); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Rival", url: "https://rival.com" }), + }), + ); + const body = await res.json(); + + expect(res.status).toBe(201); + expect(body.competitor.id).toBe("c-new"); + expect(mockCreateCompetitor).toHaveBeenCalledWith({ + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + }); + expect(mockCreateSource).not.toHaveBeenCalled(); + }); + + it("creates competitor with sources when provided", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + status: "active", + }); + mockCreateSource.mockResolvedValue({ id: "src-1" }); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Rival", + url: "https://rival.com", + sources: [{ type: "pricing", url: "https://rival.com/pricing" }], + }), + }), + ); + + expect(res.status).toBe(201); + expect(mockCreateSource).toHaveBeenCalledWith({ + companyId: "company-1", + competitorId: "c-new", + type: "pricing", + url: "https://rival.com/pricing", + config: undefined, + }); + }); + + it("accepts a `page` source when creating a competitor", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + }); + mockCreateSource.mockResolvedValue({ id: "src-1" }); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Rival", + url: "https://rival.com", + sources: [{ type: "page", url: "https://rival.com/changelog" }], + }), + }), + ); + expect(res.status).toBe(201); + }); + + it("accepts feed and sitemap sources when creating a competitor", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + }); + mockCreateSource.mockResolvedValue({ id: "src-1" }); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Rival", + url: "https://rival.com", + sources: [ + { type: "feed", url: "https://rival.com/rss" }, + { type: "sitemap", url: "https://rival.com/sitemap.xml" }, + ], + }), + }), + ); + expect(res.status).toBe(201); + }); + + it("best-effort discovery creates newly-found sources but never blocks creation", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + }); + mockCreateSource.mockResolvedValue({ id: "src-1" }); + mockDiscoverFeeds.mockResolvedValue([ + { type: "feed", url: "https://rival.com/rss" }, + // duplicate of an explicit source → must be de-duped + { type: "pricing", url: "https://rival.com/pricing" }, + ]); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Rival", + url: "https://rival.com", + sources: [{ type: "pricing", url: "https://rival.com/pricing" }], + }), + }), + ); + + expect(res.status).toBe(201); + // explicit pricing + discovered feed; the discovered duplicate pricing is skipped. + expect(mockCreateSource).toHaveBeenCalledTimes(2); + expect(mockCreateSource).toHaveBeenCalledWith({ + companyId: "company-1", + competitorId: "c-new", + type: "feed", + url: "https://rival.com/rss", + }); + }); + + it("still returns 201 when discovery throws", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockCreateCompetitor.mockResolvedValue({ + id: "c-new", + companyId: "company-1", + name: "Rival", + url: "https://rival.com", + }); + mockDiscoverFeeds.mockRejectedValue(new Error("network down")); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Rival", url: "https://rival.com" }), + }), + ); + expect(res.status).toBe(201); + }); + + it("returns 400 for invalid body (missing url)", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + + const res = await POST( + makeRequest("http://localhost/api/competitors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Rival" }), + }), + ); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.error).toBeTruthy(); + expect(mockCreateCompetitor).not.toHaveBeenCalled(); + }); +}); + +describe("POST /api/competitors/[id]/detect", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const detect = (id = "c-1") => + DETECT(makeRequest(`http://localhost/api/competitors/${id}/detect`, { method: "POST" }), { + params: Promise.resolve({ id }), + }); + + it("returns 403 DOMAIN_DISABLED when competitor domain is off", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue( + NextResponse.json( + { error: "This domain is not available on this deployment", code: "DOMAIN_DISABLED", domainId: "competitor" }, + { status: 403 }, + ), + ); + + const res = await detect(); + const body = await res.json(); + + expect(res.status).toBe(403); + expect(body.code).toBe("DOMAIN_DISABLED"); + expect(mockGetCompetitor).not.toHaveBeenCalled(); + expect(mockCreateSource).not.toHaveBeenCalled(); + }); + + it("returns 404 when the competitor does not exist", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockGetCompetitor.mockResolvedValue(undefined); + + const res = await detect("missing"); + expect(res.status).toBe(404); + expect(mockCreateSource).not.toHaveBeenCalled(); + }); + + it("discovers and creates newly-found sources, returning { created }", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockGetCompetitor.mockResolvedValue({ id: "c-1", companyId: "company-1", url: "https://rival.com" }); + mockListSources.mockResolvedValue([{ type: "sitemap", url: "https://rival.com/sitemap.xml" }]); + mockDiscoverFeeds.mockResolvedValue([ + { type: "feed", url: "https://rival.com/rss" }, + // already exists → idempotent skip + { type: "sitemap", url: "https://rival.com/sitemap.xml" }, + ]); + mockCreateSource.mockResolvedValue({ id: "src-1" }); + + const res = await detect(); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.created).toBe(1); + expect(mockCreateSource).toHaveBeenCalledTimes(1); + expect(mockCreateSource).toHaveBeenCalledWith({ + companyId: "company-1", + competitorId: "c-1", + type: "feed", + url: "https://rival.com/rss", + }); + }); + + it("is idempotent: creates nothing when all discovered sources already exist", async () => { + mockRequireWrite.mockResolvedValue(validCtx); + mockRequireDomainEnabled.mockResolvedValue(null); + mockGetCompetitor.mockResolvedValue({ id: "c-1", companyId: "company-1", url: "https://rival.com" }); + mockListSources.mockResolvedValue([{ type: "feed", url: "https://rival.com/rss" }]); + mockDiscoverFeeds.mockResolvedValue([{ type: "feed", url: "https://rival.com/rss" }]); + + const res = await detect(); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.created).toBe(0); + expect(mockCreateSource).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/app/api/competitors/route.ts b/apps/web/src/app/api/competitors/route.ts new file mode 100644 index 00000000..a9fd7759 --- /dev/null +++ b/apps/web/src/app/api/competitors/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from "next/server"; +import { revalidateTag } from "next/cache"; +import { z } from "zod"; +import { createCompetitor, listCompetitors, createSource } from "@burnless/db"; +import { requireCompanyAccess, requireCompanyWrite, parseBody, withErrorHandler } from "@/lib/api-helpers"; +import { requireDomainEnabled } from "@/lib/domain-gating"; +import { discoverFeeds } from "@/lib/competitor/discover"; + +const createCompetitorSchema = z.object({ + name: z.string().min(1), + url: z.string().url(), + sources: z + .array( + z.object({ + type: z.enum(["pricing", "social", "page", "feed", "sitemap"]), + url: z.string().url(), + config: z.unknown().optional(), + }), + ) + .optional(), +}); + +export const GET = withErrorHandler(async (_request: Request) => { + const ctx = await requireCompanyAccess(); + if ("error" in ctx) return ctx.error; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const competitors = await listCompetitors(ctx.companyId); + return NextResponse.json({ competitors }); +}); + +export const POST = withErrorHandler(async (request: Request) => { + const ctx = await requireCompanyWrite(); + if ("error" in ctx) return ctx.error; + + const gate = await requireDomainEnabled("competitor", { companyId: ctx.companyId }); + if (gate) return gate; + + const parsed = await parseBody(request, createCompetitorSchema); + if ("error" in parsed) return parsed.error; + + const { name, url, sources } = parsed.data; + const competitor = await createCompetitor({ companyId: ctx.companyId, name, url }); + + if (sources && sources.length > 0) { + for (const source of sources) { + await createSource({ + companyId: ctx.companyId, + competitorId: competitor.id, + type: source.type, + url: source.url, + config: source.config, + }); + } + } + + // Best-effort: deterministically discover feed/sitemap sources. Never blocks + // creation — a discovery failure leaves the competitor with its explicit sources. + try { + const found = await discoverFeeds(competitor.url); + const existing = new Set((sources ?? []).map((s) => `${s.type}:${s.url}`)); + for (const f of found) { + if (existing.has(`${f.type}:${f.url}`)) continue; + existing.add(`${f.type}:${f.url}`); + await createSource({ + companyId: ctx.companyId, + competitorId: competitor.id, + type: f.type, + url: f.url, + }); + } + } catch { + /* discovery is best-effort */ + } + + revalidateTag("competitors", { expire: 0 }); + return NextResponse.json({ competitor }, { status: 201 }); +}); diff --git a/apps/web/src/lib/ai-tools/__tests__/competitor.test.ts b/apps/web/src/lib/ai-tools/__tests__/competitor.test.ts new file mode 100644 index 00000000..77e32a2b --- /dev/null +++ b/apps/web/src/lib/ai-tools/__tests__/competitor.test.ts @@ -0,0 +1,153 @@ +/** + * Tests for the competitor read-only AI toolset (Task 9): + * list_competitors + list_competitor_changes. + * + * HARNESS: real PGLite via @db-test — mirrors transactions.test.ts wiring. + * The DB is real; only framework seams pulled by the import graph are mocked. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createUser, createCompany } from "@db-test/factories"; +import { createCompetitor, insertChanges } from "@burnless/db"; +import type { ToolContext } from "../types"; + +// ── Framework seam mocks ────────────────────────────────────────────────────── +vi.mock("next/cache", () => ({ + unstable_cache: (fn: (...args: unknown[]) => unknown) => fn, + revalidateTag: vi.fn(), +})); +vi.mock("react", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, cache: (fn: unknown) => fn }; +}); +vi.mock("@/lib/auth", () => ({ + auth: vi.fn().mockResolvedValue(null), +})); +vi.mock("next/headers", () => ({ + cookies: vi.fn().mockResolvedValue({ get: () => undefined }), +})); + +import { competitorHandlers, competitorTools } from "../competitor"; + +// ── Setup ───────────────────────────────────────────────────────────────────── + +let ctx: ToolContext; +let companyId: string; + +beforeEach(async () => { + const user = await createUser(); + const company = await createCompany(user.id); + companyId = company.id; + ctx = { companyId, userId: user.id }; +}); + +// ── Tool definition guard ───────────────────────────────────────────────────── + +describe("competitorTools definitions", () => { + it("declares two read-only tools (no mutates)", () => { + const names = competitorTools.map((t) => t.name).sort(); + expect(names).toEqual(["list_competitor_changes", "list_competitors"]); + expect(competitorTools.every((t) => !t.mutates)).toBe(true); + }); +}); + +// ── list_competitors ────────────────────────────────────────────────────────── + +describe("list_competitors", () => { + it("returns a friendly message when no competitors tracked", async () => { + const out = await competitorHandlers["list_competitors"]!({}, ctx); + expect(out).toMatch(/no competitors/i); + }); + + it("returns the company's competitors as text", async () => { + await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const out = await competitorHandlers["list_competitors"]!({}, ctx); + expect(out).toContain("Acme"); + expect(out).toContain("https://acme.com"); + }); + + it("returns 'No company in context.' when companyId is missing", async () => { + const noCtx: ToolContext = { userId: "u1" }; + const out = await competitorHandlers["list_competitors"]!({}, noCtx); + expect(out).toBe("No company in context."); + }); + + it("does not leak competitors from another company", async () => { + const other = await createUser(); + const otherCompany = await createCompany(other.id); + await createCompetitor({ companyId: otherCompany.id, name: "OtherCo", url: "https://other.com" }); + const out = await competitorHandlers["list_competitors"]!({}, ctx); + expect(out).not.toContain("OtherCo"); + }); +}); + +// ── list_competitor_changes ─────────────────────────────────────────────────── + +describe("list_competitor_changes", () => { + it("returns a friendly message when no changes detected", async () => { + const out = await competitorHandlers["list_competitor_changes"]!({}, ctx); + expect(out).toMatch(/no competitor changes/i); + }); + + it("returns changes for the company", async () => { + // Create a competitor + source + snapshot + change via insertChanges + const { getTestDb } = await import("@db-test/setup"); + const db = getTestDb(); + const { competitorSources, competitorSnapshots } = await import("@burnless/db"); + + const comp = await createCompetitor({ companyId, name: "Rival", url: "https://rival.com" }); + + // Insert a source directly + const [src] = await db + .insert(competitorSources) + .values({ + competitorId: comp.id, + companyId, + type: "pricing", + url: "https://rival.com/pricing", + }) + .returning(); + + // Insert a snapshot + const [snap] = await db + .insert(competitorSnapshots) + .values({ + competitorId: comp.id, + sourceId: src!.id, + companyId, + raw: "{}", + rawHash: "abc", + structured: {}, + structuredHash: "def", + }) + .returning(); + + await insertChanges([ + { + competitorId: comp.id, + sourceId: src!.id, + snapshotId: snap!.id, + companyId, + changeType: "price_change", + summary: "Starter plan dropped from 49 to 39", + severity: "high", + }, + ]); + + const out = await competitorHandlers["list_competitor_changes"]!({}, ctx); + expect(out).toContain("price_change"); + expect(out).toContain("Starter plan dropped"); + expect(out).toContain("high"); + }); + + it("respects the limit parameter", async () => { + const out = await competitorHandlers["list_competitor_changes"]!({ limit: 5 }, ctx); + // No changes → friendly message (also proves limit doesn't crash) + expect(out).toMatch(/no competitor changes/i); + }); + + it("returns 'No company in context.' when companyId is missing", async () => { + const noCtx: ToolContext = { userId: "u1" }; + const out = await competitorHandlers["list_competitor_changes"]!({}, noCtx); + expect(out).toBe("No company in context."); + }); +}); diff --git a/apps/web/src/lib/ai-tools/competitor.ts b/apps/web/src/lib/ai-tools/competitor.ts new file mode 100644 index 00000000..7e2f95a3 --- /dev/null +++ b/apps/web/src/lib/ai-tools/competitor.ts @@ -0,0 +1,73 @@ +/** + * Competitor read-only AI tools (Task 9 — competitor analysis spine). + * + * `list_competitors` — returns the company's tracked competitors. + * `list_competitor_changes` — returns recent detected changes across competitors. + * + * READ-ONLY: no `mutates`, no AI provider needed to run (deterministic DB query). + * These give the AI grounding but require no special capability. + */ + +import { z } from "zod"; +import type { ToolDefinition } from "@burnless/ai"; +import { listCompetitors, listChanges } from "@burnless/db"; +import type { ToolHandler } from "./types"; + +// ── Tool definitions ────────────────────────────────────────────────────────── + +export const competitorTools: ToolDefinition[] = [ + { + name: "list_competitors", + description: + "List the competitors tracked for this company (name, URL, status). Use this to see which competitors are being monitored and to resolve competitor names before querying changes.", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "list_competitor_changes", + description: + "List recent detected changes across tracked competitors (e.g. price changes, new pricing plans, follower count shifts). Returns the most recent changes first.", + inputSchema: { + type: "object", + properties: { + limit: { + type: "number", + description: "Maximum number of changes to return (default 20).", + }, + }, + }, + }, +]; + +// ── Zod schemas (for toolSchemas / validateToolInput in index.ts) ───────────── + +const listCompetitorsSchema = z.object({}); + +const listCompetitorChangesSchema = z.object({ + limit: z.number().int().positive().optional(), +}); + +export const competitorSchemas: Record = { + list_competitors: listCompetitorsSchema, + list_competitor_changes: listCompetitorChangesSchema, +}; + +// ── Handlers ────────────────────────────────────────────────────────────────── + +export const competitorHandlers: Record = { + async list_competitors(_input, ctx) { + if (!ctx.companyId) return "No company in context."; + const rows = await listCompetitors(ctx.companyId); + if (rows.length === 0) return "No competitors are being tracked yet."; + return rows.map((c) => `- ${c.name} (${c.url}) [${c.status}]`).join("\n"); + }, + + async list_competitor_changes(input, ctx) { + if (!ctx.companyId) return "No company in context."; + const limit = typeof input.limit === "number" ? input.limit : 20; + const rows = await listChanges(ctx.companyId, { limit }); + if (rows.length === 0) return "No competitor changes detected yet."; + return rows + .map((r) => `- [${r.severity}] ${r.changeType}: ${r.summary}`) + .join("\n"); + }, +}; diff --git a/apps/web/src/lib/ai-tools/index.ts b/apps/web/src/lib/ai-tools/index.ts index fed0642d..38b2274c 100644 --- a/apps/web/src/lib/ai-tools/index.ts +++ b/apps/web/src/lib/ai-tools/index.ts @@ -40,6 +40,7 @@ import { transactionSchemas, transactionHandlers } from "./transactions"; import { companyKnowledgeSchemas, companyKnowledgeHandlers } from "./company-knowledge"; import { skillsSchemas, skillsHandlers } from "./skills"; import { calculateSchemas, calculateHandlers } from "./calculate"; +import { competitorSchemas, competitorHandlers } from "./competitor"; // NOTE: "./mcp-describe" only — "./mcp" pulls next-auth via ai-feature-flags // and is loaded lazily inside executeToolCall instead. import { describeMcpToolAction } from "./mcp-describe"; @@ -79,6 +80,7 @@ const toolSchemas: Record = { ...companyKnowledgeSchemas, ...skillsSchemas, ...calculateSchemas, + ...competitorSchemas, }; const toolHandlers: Record = { @@ -95,6 +97,7 @@ const toolHandlers: Record = { ...companyKnowledgeHandlers, ...skillsHandlers, ...calculateHandlers, + ...competitorHandlers, }; // ── Mutation tagging (for guardrail enforcement) ──────────────────────────── diff --git a/apps/web/src/lib/api-fetch.ts b/apps/web/src/lib/api-fetch.ts index 3fd1a8ab..88bf316e 100644 --- a/apps/web/src/lib/api-fetch.ts +++ b/apps/web/src/lib/api-fetch.ts @@ -10,7 +10,7 @@ * so reintroduces a second, drift-prone source (a stale server-rendered prop or * a per-tab sessionStorage value) and causes spurious 409 ScenarioSafetyErrors. */ -import { publishMutation, domainFromUrl, FINANCIAL_DOMAINS } from "./mutation-bus"; +import { publishMutation, domainFromUrl, FINANCIAL_DOMAINS, COMPETITOR_DOMAINS } from "./mutation-bus"; const MUTATING = new Set(["POST", "PATCH", "PUT", "DELETE"]); @@ -31,10 +31,13 @@ export async function apiFetch( try { const method = (init?.method ?? "GET").toUpperCase(); const domain = domainFromUrl(url); - // Emit only financial-data mutations. Non-financial endpoints (notably the - // insights regen POST itself) map to "other" and are NOT emitted — this is what - // prevents the auto-regen from retriggering the badge/countdown in a loop. - if (res.ok && MUTATING.has(method) && FINANCIAL_DOMAINS.has(domain)) { + // Emit financial-data mutations AND competitor mutations. Non-financial endpoints + // (notably the insights regen POST itself) map to "other" and are NOT emitted — + // this is what prevents the auto-regen from retriggering the badge/countdown in a + // loop. Competitor mutations are kept separate from FINANCIAL_DOMAINS so competitor + // syncs do not reset the AI-insight stale countdown. + if (res.ok && MUTATING.has(method) && + (FINANCIAL_DOMAINS.has(domain) || COMPETITOR_DOMAINS.has(domain))) { publishMutation({ domain, method, at: Date.now() }); } } catch { diff --git a/apps/web/src/lib/competitor/__tests__/discover.test.ts b/apps/web/src/lib/competitor/__tests__/discover.test.ts new file mode 100644 index 00000000..f93b81fc --- /dev/null +++ b/apps/web/src/lib/competitor/__tests__/discover.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { discoverFeeds } from "../discover"; + +function stubFetch(map: Record) { + vi.stubGlobal("fetch", vi.fn(async (url: string) => { + const hit = map[url.toString()]; + if (!hit) return { ok: false, status: 404, text: async () => "", headers: { get: () => null } } as never; + return { ok: true, status: hit.status ?? 200, text: async () => hit.body, headers: { get: () => "text/html" } } as never; + })); +} +afterEach(() => vi.unstubAllGlobals()); + +describe("discoverFeeds", () => { + it("finds a feed and a robots.txt sitemap", async () => { + stubFetch({ + "https://acme.com/": { body: `` }, + "https://acme.com/robots.txt": { body: "User-agent: *\nSitemap: https://acme.com/sitemap.xml" }, + }); + const out = await discoverFeeds("https://acme.com/"); + expect(out).toContainEqual({ type: "feed", url: "https://acme.com/blog/rss" }); + expect(out).toContainEqual({ type: "sitemap", url: "https://acme.com/sitemap.xml" }); + }); + + it("falls back to /sitemap.xml when robots has none, and never throws on fetch failure", async () => { + stubFetch({ + "https://acme.com/": { body: "no feed" }, + // robots.txt missing → 404; fallback probe: + "https://acme.com/sitemap.xml": { body: `https://acme.com/` }, + }); + const out = await discoverFeeds("https://acme.com/"); + expect(out).toEqual([{ type: "sitemap", url: "https://acme.com/sitemap.xml" }]); + }); +}); diff --git a/apps/web/src/lib/competitor/__tests__/pipeline.test.ts b/apps/web/src/lib/competitor/__tests__/pipeline.test.ts new file mode 100644 index 00000000..e32efff3 --- /dev/null +++ b/apps/web/src/lib/competitor/__tests__/pipeline.test.ts @@ -0,0 +1,269 @@ +/** + * Integration test for the reframed competitor sync pipeline (spec §5). + * + * DB WIRING: imports @db-test factories → vitest.config.mts `needsDb()` detects + * this file as a "db" test → vitest.setup.db.ts runs first and assigns + * globalThis.__burnless_db to a fresh PGlite instance BEFORE @burnless/db + * evaluates. When pipeline.ts (and its @burnless/db imports) are loaded, they + * pick up the in-memory PGlite. The @burnless/db barrel is NOT mocked. + * + * NETWORK: we do NOT mock ../collectors — the REAL collectors + real + * `normalizeHtml`/parse run. Both the structured collectors and the `page` + * fallback fetch through `httpFetch`, which calls `global.fetch`. We stub + * `global.fetch` with a sequenced-HTML helper so runs return controlled pages + * (nonce churn, visible changes, confident pricing) with zero network I/O. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { eq } from "drizzle-orm"; +import { createUser, createCompany, createMember } from "@db-test/factories"; +import { + db, + sha256hex, + competitorSnapshots, + createCompetitor, + createSource, + getLatestSnapshot, + listChanges, +} from "@burnless/db"; + +// Import the real pipeline (collectors are real, only global.fetch is stubbed). +import { runSource } from "../pipeline"; + +/** Stub global.fetch to return each HTML page in turn (last page repeats). */ +function stubFetchSequence(pages: string[]): void { + let i = 0; + vi.stubGlobal( + "fetch", + vi.fn(async () => { + const raw = pages[Math.min(i, pages.length - 1)] ?? ""; + i += 1; + return { + status: 200, + headers: { get: () => "text/html" }, + text: async () => raw, + } as unknown as Response; + }), + ); +} + +/** Count stored snapshots for a source (store-on-change assertions). */ +async function countSnapshots(sourceId: string): Promise { + const rows = await db + .select() + .from(competitorSnapshots) + .where(eq(competitorSnapshots.sourceId, sourceId)); + return rows.length; +} + +/** HTML the pricing collector parses confidently (JSON-LD Product + visible price). */ +function pricingHtml(amount: number): string { + const ld = JSON.stringify({ + "@type": "Product", + name: "Pro", + offers: { price: amount, priceCurrency: "USD" }, + }); + return `
  • Pro $${amount}/mo
`; +} + +describe("competitor sync pipeline", () => { + let companyId: string; + let competitorId: string; + let sourceRow: Awaited>; + let pageSource: Awaited>; + let feedSource: Awaited>; + let sitemapSource: Awaited>; + + beforeEach(async () => { + const user = await createUser(); + const company = await createCompany(user.id); + // createMember defaults to role:"owner" → getCompanyNotifyUserIds returns this userId + await createMember(company.id, user.id); + companyId = company.id; + + const competitor = await createCompetitor({ + companyId, + name: "Acme", + url: "https://acme.com", + }); + competitorId = competitor.id; + + sourceRow = await createSource({ + companyId, + competitorId: competitor.id, + type: "pricing", + url: "https://acme.com/pricing", + }); + // A `page`-type source has NO collector → Tier 1 (content diff) only. + pageSource = await createSource({ + companyId, + competitorId: competitor.id, + type: "page", + url: "https://acme.com/features", + }); + feedSource = await createSource({ + companyId, + competitorId: competitor.id, + type: "feed", + url: "https://acme.com/feed.xml", + }); + sitemapSource = await createSource({ + companyId, + competitorId: competitor.id, + type: "sitemap", + url: "https://acme.com/sitemap.xml", + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("first run stores a snapshot and returns changed:true", async () => { + stubFetchSequence([pricingHtml(29)]); + + const result = await runSource(sourceRow); + + expect(result.changed).toBe(true); + expect(result.broken).toBe(false); + + const snap = await getLatestSnapshot(sourceRow.id); + expect(snap).toBeTruthy(); + expect(snap!.structuredHash).toBeTruthy(); + expect(snap!.normalizedHash).toBeTruthy(); + }); + + it("unchanged re-run creates NO new snapshot (store-on-change guard)", async () => { + stubFetchSequence([pricingHtml(29), pricingHtml(29)]); + + // First run — stores snapshot + await runSource(sourceRow); + const snapAfterFirst = await getLatestSnapshot(sourceRow.id); + + // Second run — same data, must not create another snapshot + const result = await runSource(sourceRow); + expect(result.changed).toBe(false); + expect(result.broken).toBe(false); + + const snapAfterSecond = await getLatestSnapshot(sourceRow.id); + // Still the same snapshot row + expect(snapAfterSecond!.id).toBe(snapAfterFirst!.id); + expect(await countSnapshots(sourceRow.id)).toBe(1); + }); + + it("changed price stores a new snapshot and emits a price_increase change row", async () => { + stubFetchSequence([pricingHtml(29), pricingHtml(39)]); + + // Run 1: establish baseline at price 29 + await runSource(sourceRow); + + // Run 2: price bumped to 39 — should diff and emit price_increase + const result = await runSource(sourceRow); + + expect(result.changed).toBe(true); + expect(result.broken).toBe(false); + expect(result.alerts.length).toBeGreaterThan(0); + expect(result.alerts.some((a) => a.changeType === "price_increase")).toBe(true); + + const changes = await listChanges(companyId); + expect(changes.length).toBeGreaterThan(0); + expect(changes.some((c) => c.changeType === "price_increase")).toBe(true); + }); + + it("does NOT store a new snapshot when only a nonce changed (normalized content identical)", async () => { + // Identical VISIBLE text, rotated nonce attribute between the two fetches. + const pageA = `
  • Pro $29
`; + const pageB = `
  • Pro $29
`; + stubFetchSequence([pageA, pageB]); + + await runSource(sourceRow); // first run stores baseline + const before = await countSnapshots(sourceRow.id); + const r = await runSource(sourceRow); // second run: nonce-only churn + const after = await countSnapshots(sourceRow.id); + + expect(after).toBe(before); + expect(r.changed).toBe(false); + expect(r.broken).toBe(false); + }); + + it("re-baselines silently when the prior snapshot has NULL normalized (legacy row)", async () => { + // Seed a legacy snapshot (pre-reframe migration): normalized == null. + // Insert directly — insertSnapshot() now requires normalized as a string. + await db.insert(competitorSnapshots).values({ + companyId, + competitorId, + sourceId: pageSource.id, + raw: "
Old content
", + rawHash: sha256hex("
Old content
"), + structured: {}, + structuredHash: sha256hex(JSON.stringify({})), + normalized: null, + normalizedHash: null, + }); + + stubFetchSequence([`
Fresh content — SSO
`]); + const r = await runSource(pageSource); + + // The new snapshot IS stored (re-baseline), but NO content_changed alert fires. + expect(r.changed).toBe(true); + expect(r.broken).toBe(false); + expect(await countSnapshots(pageSource.id)).toBe(2); + + const changes = await listChanges(companyId, { competitorId }); + expect(changes.some((c) => c.changeType === "content_changed")).toBe(false); + }); + + it("emits ONE content change on a real visible change (page source, no structured parse)", async () => { + const pageA = `
Welcome
`; + const pageB = `
Welcome — now with SSO
`; + stubFetchSequence([pageA, pageB]); + + await runSource(pageSource); // a source of type "page" (no collector) + const r = await runSource(pageSource); + + expect(r.changed).toBe(true); + expect(r.broken).toBe(false); + + const changes = await listChanges(companyId, { competitorId }); + const content = changes.filter((c) => c.changeType === "content_changed"); + expect(content).toHaveLength(1); + expect(content[0]!.severity).toBe("info"); + // no typed pricing/social change on a page source + expect(changes.every((c) => c.changeType === "content_changed")).toBe(true); + }); + + it("emits BOTH a content change and a typed price change when pricing parse is confident", async () => { + stubFetchSequence([pricingHtml(29), pricingHtml(39)]); + + await runSource(sourceRow); + const r = await runSource(sourceRow); + + const changes = await listChanges(companyId, { competitorId }); + expect(changes.some((c) => c.changeType === "content_changed")).toBe(true); + expect(changes.some((c) => c.changeType === "price_increase")).toBe(true); + expect(r.changed).toBe(true); + }); + + it("emits post_published when a feed gains an item", async () => { + const feedA = `Firsthttps://x/1g1`; + const feedB = `Firsthttps://x/1g1Secondhttps://x/2g2`; + stubFetchSequence([feedA, feedB]); + await runSource(feedSource); // baseline + await runSource(feedSource); // adds g2 + const changes = await listChanges(companyId, { competitorId }); + expect( + changes.some((c) => c.changeType === "post_published" && c.summary === "New post: Second"), + ).toBe(true); + }); + + it("emits page_added when a sitemap gains a url", async () => { + const smA = `https://x/`; + const smB = `https://x/https://x/new`; + stubFetchSequence([smA, smB]); + await runSource(sitemapSource); + await runSource(sitemapSource); + const changes = await listChanges(companyId, { competitorId }); + expect( + changes.some((c) => c.changeType === "page_added" && c.summary === "Page added: https://x/new"), + ).toBe(true); + }); +}); diff --git a/apps/web/src/lib/competitor/collectors/__tests__/feed.test.ts b/apps/web/src/lib/competitor/collectors/__tests__/feed.test.ts new file mode 100644 index 00000000..3a08b982 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/feed.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { feedCollector } from "../feed"; + +const load = (f: string) => readFileSync(join(__dirname, "fixtures", f), "utf8"); +const raw = (body: string) => ({ contentType: "application/xml", raw: body, fetchedAt: new Date(), status: 200 }); +const src = { id: "s1", type: "feed", url: "https://acme.com/rss" }; + +describe("feedCollector.parse", () => { + it("parses RSS 2.0 into id-keyed items", () => { + const r = feedCollector.parse(raw(load("rss.xml")), src); + expect(r.confidence).toBe(0.9); + expect(Object.keys(r.structured.items as object).sort()).toEqual(["g-1", "g-2"]); + const i = (r.structured.items as Record)["g-2"]!; + expect(i.title).toBe("Second & Post"); + expect(i.publishedAt).toBe("2026-07-02T10:00:00.000Z"); + }); + + it("parses Atom entries (href link + id + updated)", () => { + const r = feedCollector.parse(raw(load("atom.xml")), src); + expect(r.confidence).toBe(0.9); + const items = r.structured.items as Record; + expect(items["tag:acme,2026:1"]!.title).toBe("Atom Post"); + expect(items["tag:acme,2026:1"]!.link).toBe("https://acme.com/a/1"); + }); + + it("returns low confidence for a non-feed / empty document", () => { + expect(feedCollector.parse(raw("no feed"), src).confidence).toBe(0.1); + expect(feedCollector.parse(raw(""), src).confidence).toBe(0.1); + }); +}); diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/atom.xml b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/atom.xml new file mode 100644 index 00000000..5b68b0e4 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/atom.xml @@ -0,0 +1,3 @@ +Blog +Atom Posttag:acme,2026:12026-07-03T10:00:00Z + diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-broken.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-broken.html new file mode 100644 index 00000000..e57d811c --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-broken.html @@ -0,0 +1,2 @@ + +Test fixtureComing soon diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom-noise.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom-noise.html new file mode 100644 index 00000000..ea7f9e6d --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom-noise.html @@ -0,0 +1,17 @@ +Test fixture +
$0.40
+
$0.01
+
$0.10
+
$1.20
+
$0.05
+
$2.50
+
$0.80
+
$0.15
+
$0.30
+
$3.00
+
$0.60
+
$0.20
+
$5.00
+
$0.90
+
$1.00
+ diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom.html new file mode 100644 index 00000000..d410ddcd --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-dom.html @@ -0,0 +1,5 @@ + +Test fixture +

Starter

$9/mo
+

Pro

$29/mo
+ diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld-noprice.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld-noprice.html new file mode 100644 index 00000000..198f0695 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld-noprice.html @@ -0,0 +1,4 @@ + +Test fixture + +

Pricing

diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld.html new file mode 100644 index 00000000..53740835 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/pricing-jsonld.html @@ -0,0 +1,7 @@ + + +Test fixture + +

Pricing

diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/rss.xml b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/rss.xml new file mode 100644 index 00000000..4f687ae2 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/rss.xml @@ -0,0 +1,4 @@ +Blog +Hello Worldhttps://acme.com/p/1g-1Tue, 01 Jul 2026 10:00:00 GMT +<![CDATA[Second & Post]]>https://acme.com/p/2g-2Wed, 02 Jul 2026 10:00:00 GMT + diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/sitemap.xml b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/sitemap.xml new file mode 100644 index 00000000..604752e5 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/sitemap.xml @@ -0,0 +1,4 @@ + +https://acme.com/2026-07-01 +https://acme.com/pricing + diff --git a/apps/web/src/lib/competitor/collectors/__tests__/fixtures/social-profile.html b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/social-profile.html new file mode 100644 index 00000000..6921fb6e --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/fixtures/social-profile.html @@ -0,0 +1,3 @@ + +Test fixture +12,300 Followers diff --git a/apps/web/src/lib/competitor/collectors/__tests__/pricing.test.ts b/apps/web/src/lib/competitor/collectors/__tests__/pricing.test.ts new file mode 100644 index 00000000..0f8d49fb --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/pricing.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { pricingCollector } from "../pricing"; + +const fx = (n: string) => readFileSync(join(__dirname, "fixtures", n), "utf8"); +const cap = (raw: string) => ({ contentType: "text/html", raw, fetchedAt: new Date(0), status: 200 }); +const src = { id: "s1", type: "pricing", url: "https://x/pricing" }; + +describe("pricingCollector.parse", () => { + it("extracts plans from JSON-LD with high confidence", () => { + const r = pricingCollector.parse(cap(fx("pricing-jsonld.html")), src); + const plans = (r.structured as any).plans; + expect(plans[0].name).toBe("Pro"); + expect(plans[0].price.amount).toBe(29); + expect(plans[0].price.currency).toBe("USD"); + expect(r.confidence).toBeGreaterThan(0.7); + }); + + it("falls back to DOM heuristics with moderate confidence", () => { + const r = pricingCollector.parse(cap(fx("pricing-dom.html")), src); + expect((r.structured as any).plans.length).toBeGreaterThan(0); + expect(r.confidence).toBeGreaterThan(0.3); + }); + + it.each([ + { fixture: "pricing-broken.html", when: "nothing parses" }, + { fixture: "pricing-jsonld-noprice.html", when: "JSON-LD Product without an offer price (no real tiers)" }, + { fixture: "pricing-dom-noise.html", when: "noisy DOM with many unnamed/implausible plan blocks" }, + ])("returns low confidence when $when", ({ fixture }) => { + const r = pricingCollector.parse(cap(fx(fixture)), src); + expect(r.confidence).toBeLessThan(0.4); + }); + + it("still parses a clean 2-tier DOM pricing page with moderate confidence", () => { + const r = pricingCollector.parse(cap(fx("pricing-dom.html")), src); + expect((r.structured as any).plans.length).toBeGreaterThan(0); + expect(r.confidence).toBeGreaterThan(0.3); + }); +}); diff --git a/apps/web/src/lib/competitor/collectors/__tests__/sitemap.test.ts b/apps/web/src/lib/competitor/collectors/__tests__/sitemap.test.ts new file mode 100644 index 00000000..8b35b116 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/sitemap.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { sitemapCollector } from "../sitemap"; + +const raw = (body: string) => ({ contentType: "application/xml", raw: body, fetchedAt: new Date(), status: 200 }); +const src = { id: "s1", type: "sitemap", url: "https://acme.com/sitemap.xml" }; + +describe("sitemapCollector.parse", () => { + it("parses a urlset into url→lastmod map + count", () => { + const body = readFileSync(join(__dirname, "fixtures", "sitemap.xml"), "utf8"); + const r = sitemapCollector.parse(raw(body), src); + expect(r.confidence).toBe(0.9); + const urls = r.structured.urls as Record; + expect(Object.keys(urls).sort()).toEqual(["https://acme.com/", "https://acme.com/pricing"]); + expect(urls["https://acme.com/"]).toBe("2026-07-01"); + expect(urls["https://acme.com/pricing"]).toBe(1); + expect(r.structured.count).toBe(2); + }); + + it("low confidence for a sitemap index (nested — out of scope)", () => { + const idx = `https://acme.com/s1.xml`; + expect(sitemapCollector.parse(raw(idx), src).confidence).toBe(0.1); + }); + + it("low confidence for an empty / non-sitemap document", () => { + expect(sitemapCollector.parse(raw(""), src).confidence).toBe(0.1); + }); +}); diff --git a/apps/web/src/lib/competitor/collectors/__tests__/social.test.ts b/apps/web/src/lib/competitor/collectors/__tests__/social.test.ts new file mode 100644 index 00000000..88b1ac8f --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/__tests__/social.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { socialCollector } from "../social"; + +const fx = (n: string) => readFileSync(join(__dirname, "fixtures", n), "utf8"); +const cap = (raw: string) => ({ contentType: "text/html", raw, fetchedAt: new Date(0), status: 200 }); + +describe("socialCollector.parse", () => { + it("extracts follower count", () => { + const r = socialCollector.parse(cap(fx("social-profile.html")), { id: "s", type: "social", url: "https://x.com/acme", config: { platform: "x", handle: "acme" } }); + expect((r.structured as any).followers).toBe(12300); + expect((r.structured as any).platform).toBe("x"); + expect(r.confidence).toBeGreaterThan(0.3); + }); + + it("returns low confidence when no follower count found", () => { + const r = socialCollector.parse(cap("nope"), { id: "s", type: "social", url: "u" }); + expect(r.confidence).toBeLessThan(0.4); + }); +}); diff --git a/apps/web/src/lib/competitor/collectors/feed.ts b/apps/web/src/lib/competitor/collectors/feed.ts new file mode 100644 index 00000000..567de186 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/feed.ts @@ -0,0 +1,78 @@ +import type { Collector, RawCapture, CollectorSource } from "./types"; +import { httpFetch } from "./types"; + +const ITEM_BLOCK = /<(item|entry)\b[\s\S]*?<\/\1>/gi; +const MAX_ITEMS = 50; + +function stripCdata(s: string): string { + return s.replace(//g, "$1"); +} +function stripTags(s: string): string { + // Fixpoint strip (CodeQL js/incomplete-multi-character-sanitization remediation): + // repeat until stable so nested / reconstructed tags (e.g. `ipt>`) + // fully collapse; the optional `>?` also removes an unterminated trailing tag + // like `]*>?/g, ""); + } + return out; +} + +function decode(s: string): string { + // Decode entities first (so `<script>` becomes markup), then fixpoint-strip + // tags — the loop guarantees no `<…` survives in the output. `&` is decoded + // last among the entities to avoid double-unescaping. The result is plain display + // text (React escapes it again on render); this is defence-in-depth, not the sink. + const decoded = stripCdata(s) + .replace(/</gi, "<") + .replace(/>/gi, ">") + .replace(/"/gi, '"') + .replace(/�*39;/g, "'") + .replace(/'/gi, "'") + .replace(/&/gi, "&"); + return stripTags(decoded).trim(); +} +function firstTag(block: string, name: string): string | null { + const m = new RegExp(String.raw`<${name}\b[^>]*>([\s\S]*?)`, "i").exec(block); + return m ? decode(m[1]!) || null : null; +} +function atomHref(block: string): string | null { + const links = [...block.matchAll(/]*?)\/?>/gi)].map((m) => m[1]!); + const pick = + links.find((a) => /rel=["']?alternate/i.test(a)) ?? + links.find((a) => !/rel=/i.test(a)) ?? + links[0]; + if (!pick) return null; + const href = /href=["']([^"']+)["']/i.exec(pick); + return href ? decode(href[1]!) : null; +} +function toIso(s: string | null): string | null { + if (!s) return null; + const t = Date.parse(s); + return Number.isNaN(t) ? null : new Date(t).toISOString(); +} + +export const feedCollector: Collector = { + type: "feed", + fetch: (source: CollectorSource): Promise => httpFetch(source.url), + parse: (raw: RawCapture) => { + const items: Record = {}; + for (const block of raw.raw.match(ITEM_BLOCK) ?? []) { + if (Object.keys(items).length >= MAX_ITEMS) break; + const isAtom = /^ 0 ? 0.9 : 0.1 }; + }, +}; diff --git a/apps/web/src/lib/competitor/collectors/index.ts b/apps/web/src/lib/competitor/collectors/index.ts new file mode 100644 index 00000000..6eac7445 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/index.ts @@ -0,0 +1,18 @@ +import type { Collector } from "./types"; +import { pricingCollector } from "./pricing"; +import { socialCollector } from "./social"; +import { feedCollector } from "./feed"; +import { sitemapCollector } from "./sitemap"; + +const COLLECTORS: Record = { + [pricingCollector.type]: pricingCollector, + [socialCollector.type]: socialCollector, + [feedCollector.type]: feedCollector, + [sitemapCollector.type]: sitemapCollector, +}; + +export function getCollector(type: string): Collector | null { + return COLLECTORS[type] ?? null; +} + +export * from "./types"; diff --git a/apps/web/src/lib/competitor/collectors/pricing.ts b/apps/web/src/lib/competitor/collectors/pricing.ts new file mode 100644 index 00000000..9222c792 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/pricing.ts @@ -0,0 +1,105 @@ +import type { Collector, ParseResult, RawCapture } from "./types"; +import { httpFetch } from "./types"; + +interface Plan { + name: string; + price: { amount: number | null; currency: string | null; period: string | null }; + features: string[]; +} + +// Module-level regex constants (all non-global: exec() always starts at position 0). +const PRICE_RE = /([$€£]|USD|EUR|GBP)?\s*(\d+(?:\.\d+)?)\s*(?:\/\s*(month|mo|year|yr))?/i; +const H1_RE = /]*>([^<]+)<\/h[1-6]>/i; +const PRICE_BLOCK_RE = /[$€£]\s*\d[\d.,]*\s*(?:\/\s*(?:month|mo|year|yr))?/i; +const SYMBOL_TO_CURRENCY: Record = { $: "USD", "€": "EUR", "£": "GBP" }; + +function resolvePeriod(raw: string | undefined): "year" | "month" | null { + if (!raw) return null; + return raw.startsWith("y") ? "year" : "month"; +} + +function parsePrice(s: string): { amount: number | null; currency: string | null; period: string | null } { + const m = PRICE_RE.exec(s.replaceAll(",", "")); + if (!m) return { amount: null, currency: null, period: null }; + return { + amount: Number(m[2]), + currency: m[1] ? (SYMBOL_TO_CURRENCY[m[1]] ?? m[1].toUpperCase()) : null, + period: resolvePeriod(m[3]), + }; +} + +type JsonObj = Record; + +function jsonLdNodeToPlan(raw: unknown): Plan | null { + if (raw == null || typeof raw !== "object") return null; + const node = raw as JsonObj; + if (node["@type"] !== "Product" || !node.offers) return null; + const offersRaw = Array.isArray(node.offers) ? node.offers[0] : node.offers; + const offer = (offersRaw ?? {}) as JsonObj; + return { + name: String(node.name ?? "Plan"), + price: { + amount: offer.price != null ? Number(offer.price) : null, + currency: (offer.priceCurrency as string | undefined) ?? null, + period: null, + }, + features: [], + }; +} + +function fromJsonLd(html: string): Plan[] { + const plans: Plan[] = []; + const re = /]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; + let m: RegExpExecArray | null; + while ((m = re.exec(html))) { + try { + const data = JSON.parse((m[1] ?? "").trim()) as unknown; + const nodes: unknown[] = Array.isArray(data) ? data : [data]; + for (const node of nodes) { + const plan = jsonLdNodeToPlan(node); + if (plan) plans.push(plan); + } + } catch { + /* ignore malformed block */ + } + } + return plans; +} + +function fromDomHeuristic(html: string): Plan[] { + const plans: Plan[] = []; + const blockRe = /<(?:div|section|li)[^>]*class=["'][^"']*plan[^"']*["'][\s\S]*?<\/(?:div|section|li)>/gi; + const blocks = html.match(blockRe) ?? []; + for (const b of blocks) { + const name = (H1_RE.exec(b)?.[1] ?? "Plan").trim(); + const priceText = PRICE_BLOCK_RE.exec(b)?.[0]; + if (priceText) plans.push({ name, price: parsePrice(priceText), features: [] }); + } + return plans; +} + +/** A plan is "well-formed" for DOM blocks iff it has a real name (not the generic fallback) AND a numeric price. */ +function isWellFormedDom(p: Plan): boolean { + return p.name !== "Plan" && p.price.amount !== null; +} + +export const pricingCollector: Collector = { + type: "pricing", + fetch: (source) => httpFetch(source.url), + parse(raw: RawCapture): ParseResult { + // JSON-LD path: keep only Products whose offer has a numeric price. + // If JSON-LD Products were found but none have a price → 0.1 (priceless Product, e.g. Sentry-style). + const jsonld = fromJsonLd(raw.raw); + if (jsonld.length > 0) { + const priced = jsonld.filter((p) => p.price.amount !== null); + return { structured: { plans: priced }, confidence: priced.length > 0 ? 0.9 : 0.1 }; + } + + // DOM-heuristic path: keep only blocks with a real name AND a price. + // Implausibly many well-formed plans (> 8) = noise, not a real pricing page. + const dom = fromDomHeuristic(raw.raw); + const wellFormed = dom.filter(isWellFormedDom); + const confidence = wellFormed.length === 0 || wellFormed.length > 8 ? 0.1 : 0.5; + return { structured: { plans: wellFormed }, confidence }; + }, +}; diff --git a/apps/web/src/lib/competitor/collectors/sitemap.ts b/apps/web/src/lib/competitor/collectors/sitemap.ts new file mode 100644 index 00000000..258a7dd1 --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/sitemap.ts @@ -0,0 +1,36 @@ +import type { Collector, RawCapture, CollectorSource } from "./types"; +import { httpFetch } from "./types"; + +const URL_BLOCK = //gi; +const LOC = /([\s\S]*?)<\/loc>/i; +const LASTMOD = /([\s\S]*?)<\/lastmod>/i; +const MAX_URLS = 2000; + +function decode(s: string): string { + return s.replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&").trim(); +} + +export const sitemapCollector: Collector = { + type: "sitemap", + fetch: (source: CollectorSource): Promise => httpFetch(source.url), + parse: (raw: RawCapture) => { + // Sitemap INDEX (list of child sitemaps) is not fetched-through in this MVP. + if (/]/i.test(raw.raw)) { + return { structured: { urls: {}, count: 0 }, confidence: 0.1 }; + } + const urls: Record = {}; + let truncated = false; + for (const block of raw.raw.match(URL_BLOCK) ?? []) { + if (Object.keys(urls).length >= MAX_URLS) { truncated = true; break; } + const loc = LOC.exec(block)?.[1]; + if (!loc) continue; + const url = decode(loc); + if (!url) continue; + const lastmod = LASTMOD.exec(block)?.[1]; + urls[url] = lastmod ? decode(lastmod) : 1; + } + const count = Object.keys(urls).length; + const structured = truncated ? { urls, count, truncated: true } : { urls, count }; + return { structured, confidence: count > 0 ? 0.9 : 0.1 }; + }, +}; diff --git a/apps/web/src/lib/competitor/collectors/social.ts b/apps/web/src/lib/competitor/collectors/social.ts new file mode 100644 index 00000000..2ede40fc --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/social.ts @@ -0,0 +1,36 @@ +import type { Collector, ParseResult, RawCapture, CollectorSource } from "./types"; +import { httpFetch } from "./types"; + +// Non-global regex: exec() always starts at position 0. +// \d+\.\d+|\d+ avoids nested quantifiers (eliminates S8786 backtracking risk). +// [km] with /i flag covers K/M/k/m without duplicate chars (S5869). +const FOLLOWERS_RE = /(\d+\.\d+|\d+)([km])?\s*followers/i; + +function getMultiplier(suffix: string | undefined): number { + if (!suffix) return 1; + const lower = suffix.toLowerCase(); + if (lower === "k") return 1_000; + if (lower === "m") return 1_000_000; + return 1; +} + +function parseFollowers(html: string): number | null { + const m = FOLLOWERS_RE.exec(html.replaceAll(",", "")); + if (!m) return null; + const base = Number(m[1]); + const mult = getMultiplier(m[2]); + return Math.round(base * mult); +} + +export const socialCollector: Collector = { + type: "social", + fetch: (source) => httpFetch(source.url), + parse(raw: RawCapture, source: CollectorSource): ParseResult { + const cfg = (source.config ?? {}) as { platform?: string; handle?: string }; + const followers = parseFollowers(raw.raw); + return { + structured: { platform: cfg.platform ?? null, handle: cfg.handle ?? null, followers, posts: null }, + confidence: followers != null ? 0.6 : 0.1, + }; + }, +}; diff --git a/apps/web/src/lib/competitor/collectors/types.ts b/apps/web/src/lib/competitor/collectors/types.ts new file mode 100644 index 00000000..113638ef --- /dev/null +++ b/apps/web/src/lib/competitor/collectors/types.ts @@ -0,0 +1,46 @@ +import type { StructuredPayload } from "@burnless/engine"; + +export interface RawCapture { + contentType: string; + raw: string; + fetchedAt: Date; + status: number; +} + +export interface ParseResult { + structured: StructuredPayload; + confidence: number; +} + +export interface CollectorSource { + id: string; + type: string; + url: string; + config?: unknown; +} + +export interface Collector { + type: string; + fetch(source: CollectorSource): Promise; + parse(raw: RawCapture, source: CollectorSource): ParseResult; +} + +export async function httpFetch(url: string): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 15_000); + try { + const res = await fetch(url, { + signal: controller.signal, + headers: { "user-agent": "BurnlessCompetitorBot/1.0 (+https://burnless.ai)" }, + }); + const raw = await res.text(); + return { + contentType: res.headers.get("content-type") ?? "text/html", + raw, + fetchedAt: new Date(), + status: res.status, + }; + } finally { + clearTimeout(timer); + } +} diff --git a/apps/web/src/lib/competitor/discover.ts b/apps/web/src/lib/competitor/discover.ts new file mode 100644 index 00000000..9527015f --- /dev/null +++ b/apps/web/src/lib/competitor/discover.ts @@ -0,0 +1,100 @@ +import { httpFetch } from "./collectors/types"; + +type Candidate = { type: "feed" | "sitemap"; url: string }; + +/** Resolve a candidate URL (possibly relative) against the site base, deduplicate, and add to out. */ +function resolveCandidate( + type: Candidate["type"], + url: string, + base: string, + out: Candidate[], + seen: Set, +): void { + let abs: string; + try { + abs = new URL(url, base).toString(); + } catch { + return; + } + const key = `${type}:${abs}`; + if (!seen.has(key)) { + seen.add(key); + out.push({ type, url: abs }); + } +} + +/** Probe the homepage for feed declarations. */ +async function probeHomeFeeds( + siteUrl: string, + out: Candidate[], + seen: Set, +): Promise { + try { + const home = await httpFetch(siteUrl); + for (const m of home.raw.matchAll(/]*)>/gi)) { + const attrs = m[1]!; + if ( + /rel=["']?alternate/i.test(attrs) && + /type=["']application\/(rss|atom)\+xml["']/i.test(attrs) + ) { + const href = /href=["']([^"']+)["']/i.exec(attrs)?.[1]; + if (href) resolveCandidate("feed", href, siteUrl, out, seen); + } + } + } catch { + /* ignore */ + } +} + +/** Probe robots.txt for Sitemap: lines; returns true when at least one was found. */ +async function probeRobotsSitemap( + siteUrl: string, + out: Candidate[], + seen: Set, +): Promise { + let found = false; + try { + const robots = await httpFetch(new URL("/robots.txt", siteUrl).toString()); + for (const line of robots.raw.split(/\r?\n/)) { + const m = /^\s*sitemap:\s*(\S+)/i.exec(line); + if (m) { + resolveCandidate("sitemap", m[1]!, siteUrl, out, seen); + found = true; + } + } + } catch { + /* ignore */ + } + return found; +} + +/** Fallback probe: fetch /sitemap.xml directly and accept if it looks valid. */ +async function probeFallbackSitemap( + siteUrl: string, + out: Candidate[], + seen: Set, +): Promise { + try { + const url = new URL("/sitemap.xml", siteUrl).toString(); + const res = await httpFetch(url); + if (res.status === 200 && /<(urlset|sitemapindex)[\s>]/i.test(res.raw)) { + resolveCandidate("sitemap", url, siteUrl, out, seen); + } + } catch { + /* ignore */ + } +} + +/** + * Deterministically discover a competitor's RSS/Atom feed(s) + sitemap from + * their site URL. No AI. Best-effort: every fetch is guarded so a failure skips + * that source rather than throwing. Returns absolute, de-duped candidates. + */ +export async function discoverFeeds(siteUrl: string): Promise { + const out: Candidate[] = []; + const seen = new Set(); + await probeHomeFeeds(siteUrl, out, seen); + const hadRobotsSitemap = await probeRobotsSitemap(siteUrl, out, seen); + if (!hadRobotsSitemap) await probeFallbackSitemap(siteUrl, out, seen); + return out; +} diff --git a/apps/web/src/lib/competitor/notify.ts b/apps/web/src/lib/competitor/notify.ts new file mode 100644 index 00000000..b26a3a4b --- /dev/null +++ b/apps/web/src/lib/competitor/notify.ts @@ -0,0 +1,20 @@ +import type { Alert, Severity } from "@burnless/engine"; + +const RANK: Record = { info: 0, success: 1, warning: 2, error: 3 }; + +/** + * Reduce a list of competitor alerts into a single digest notification payload. + * The digest severity is the highest severity across all alerts. + */ +export function buildDigest( + competitorName: string, + alerts: Alert[], +): { title: string; body: string; severity: Severity } { + const severity = alerts.reduce( + (acc, a) => (RANK[a.severity] > RANK[acc] ? a.severity : acc), + "info", + ); + const title = `${alerts.length} change${alerts.length === 1 ? "" : "s"} at ${competitorName}`; + const body = alerts.map((a) => `• ${a.summary}`).join("\n"); + return { title, body, severity }; +} diff --git a/apps/web/src/lib/competitor/pipeline.ts b/apps/web/src/lib/competitor/pipeline.ts new file mode 100644 index 00000000..02b7cf2c --- /dev/null +++ b/apps/web/src/lib/competitor/pipeline.ts @@ -0,0 +1,206 @@ +import { + sha256hex, + getDueSources, + getLatestSnapshot, + insertSnapshot, + insertChanges, + updateSource, + getCompetitor, + createNotification, + getCompanyNotifyUserIds, +} from "@burnless/db"; +import { + diffStructured, + evaluateRules, + diffText, + normalizeHtml, + contentChangeAlert, + type Alert, +} from "@burnless/engine"; +import type { CompetitorSource } from "@burnless/db"; +import { getCollector, httpFetch, type CollectorSource, type RawCapture } from "./collectors"; +import { buildDigest } from "./notify"; + +/** Human page label for the Tier-1 content-change summary. */ +const SOURCE_LABELS: Record = { + pricing: "Pricing page", + social: "Social page", + page: "Page", + feed: "Feed", + sitemap: "Sitemap", +}; +function sourceLabel(type: string): string { + return SOURCE_LABELS[type] ?? "Page"; +} + +/** + * Persist change alerts to the DB and fan-out in-app notifications to all + * company owners/admins. Extracted from runSource to reduce cognitive complexity. + */ +async function persistAlertsAndNotify( + source: CompetitorSource, + snapshotId: string, + alerts: Alert[], +): Promise { + if (alerts.length === 0) return; + + await insertChanges( + alerts.map((a) => ({ + companyId: source.companyId, + competitorId: source.competitorId, + sourceId: source.id, + snapshotId, + changeType: a.changeType, + summary: a.summary, + before: a.before ?? null, + after: a.after ?? null, + severity: a.severity, + })), + ); + + const competitor = await getCompetitor(source.competitorId, source.companyId); + const digest = buildDigest(competitor?.name ?? "competitor", alerts); + const userIds = await getCompanyNotifyUserIds(source.companyId); + for (const userId of userIds) { + await createNotification({ + companyId: source.companyId, + userId, + category: "competitor", + title: digest.title, + body: digest.body, + severity: digest.severity, + link: `/competitors/${source.competitorId}`, + }); + } +} + +/** + * Run a single competitor source through the reframed pipeline (spec §5): + * fetch → Tier 1 (normalize → content hash → store-on-change → text-diff → + * generic `content` change) → Tier 2 (confident structured parse → typed + * changes) → rules → insert changes → notify. + * + * Tier 1 ALWAYS runs (source-type-agnostic; `page` sources have no collector). + * Store-on-change is keyed on the NORMALIZED content hash (not rawHash), so + * nonce/CSRF/analytics churn in the raw HTML no longer counts as a change. + */ +export async function runSource( + source: CompetitorSource, +): Promise<{ changed: boolean; broken: boolean; alerts: Alert[] }> { + const collector = getCollector(source.type); + + // ── Fetch (Tier 1 needs the raw regardless of a structured collector). ── + let raw: RawCapture; + try { + raw = collector + ? await collector.fetch(source as unknown as CollectorSource) + : await httpFetch(source.url); + } catch (e) { + await updateSource(source.id, source.companyId, { + lastRunAt: new Date(), + lastStatus: `fetch_error: ${(e as Error).message}`, + healthState: "broken", + }); + return { changed: false, broken: true, alerts: [] }; + } + + // ── Tier 1: normalize → content hash. ── + const normalized = normalizeHtml(raw.raw); + const normalizedHash = sha256hex(normalized); + + // ── Tier 2 (optional, confidence-gated): structured extraction. ── + let structured: unknown = {}; + let parseConfident = false; + if (collector) { + const parsed = collector.parse(raw, source as unknown as CollectorSource); + if (parsed.confidence >= 0.4) { + structured = parsed.structured; + parseConfident = true; + } + } + const structuredHash = sha256hex(JSON.stringify(structured)); + + const latest = await getLatestSnapshot(source.id); + const okStatus = parseConfident ? "ok" : "content_only"; + + // ── Store-on-change: skip only when BOTH content AND structured are same. ── + if ( + latest && + latest.normalizedHash === normalizedHash && + latest.structuredHash === structuredHash + ) { + await updateSource(source.id, source.companyId, { + lastRunAt: new Date(), + lastStatus: okStatus, + healthState: "ok", + }); + return { changed: false, broken: false, alerts: [] }; + } + + const snapshot = await insertSnapshot({ + companyId: source.companyId, + competitorId: source.competitorId, + sourceId: source.id, + raw: raw.raw, + rawHash: sha256hex(raw.raw), + structured, + structuredHash, + normalized, + normalizedHash, + }); + + const alerts: Alert[] = []; + if (latest) { + // Tier 1 — content change, only when a prior NORMALIZED baseline exists. + // A legacy snapshot from before the reframe migration has normalized == null; + // that's a pure one-time re-baseline, not a real content change — don't alert. + if (latest.normalized != null) { + const contentAlert = contentChangeAlert( + diffText(latest.normalized, normalized), + sourceLabel(source.type), + ); + if (contentAlert) alerts.push(contentAlert); + } + + // Tier 2 — typed structured changes, only when we have a confident parse. + if (parseConfident) { + alerts.push(...evaluateRules(source.type, diffStructured(latest.structured, structured))); + } + } + + await persistAlertsAndNotify(source, snapshot.id, alerts); + + await updateSource(source.id, source.companyId, { + lastRunAt: new Date(), + lastStatus: okStatus, + healthState: "ok", + }); + return { changed: true, broken: false, alerts }; +} + +/** + * Run all competitor sources that are currently due. + * Called by the scheduler (Task 8) and the /api/competitor/sync route (Task 12). + */ +export async function runDueCompetitorSyncs( + now: Date, +): Promise<{ ok: boolean; summary: string }> { + const due = await getDueSources(now); + let changed = 0; + let broken = 0; + + for (const s of due) { + try { + const r = await runSource(s); + if (r.changed) changed++; + if (r.broken) broken++; + } catch { + broken++; + } + } + + return { + ok: true, + summary: `${due.length} due, ${changed} changed, ${broken} need attention`, + }; +} diff --git a/apps/web/src/lib/domains/__tests__/competitor-module.test.ts b/apps/web/src/lib/domains/__tests__/competitor-module.test.ts new file mode 100644 index 00000000..d7b57829 --- /dev/null +++ b/apps/web/src/lib/domains/__tests__/competitor-module.test.ts @@ -0,0 +1,55 @@ +/** + * competitor domain module — shape test (Task 10). + * + * Proves: id, non-core, exactly the two read tools, and a nav entry exist. + * Mocks mirror company-knowledge.test.ts for transitively-imported DB deps. + */ + +import { describe, it, expect, vi } from "vitest"; + +// ── DB stub: competitor.ts imports listChanges; ai-tools/competitor imports both ── +vi.mock("@burnless/db", () => ({ + listChanges: vi.fn(async () => []), + listCompetitors: vi.fn(async () => []), +})); + +// ── From the brief (Task 10) ────────────────────────────────────────────────── + +describe("competitorDomainModule — structural", () => { + it("is a non-core domain with id 'competitor' and the two read tools", async () => { + const { competitorDomainModule } = await import("../competitor"); + expect(competitorDomainModule.id).toBe("competitor"); + expect(competitorDomainModule.core).toBeFalsy(); + expect(competitorDomainModule.tools.map((t) => t.name).sort()).toEqual([ + "list_competitor_changes", + "list_competitors", + ]); + expect(competitorDomainModule.navEntries.length).toBeGreaterThan(0); + }); + + it("navEntries items conform to DomainNavEntry shape (id, href, label, icon as string)", async () => { + const { competitorDomainModule } = await import("../competitor"); + for (const entry of competitorDomainModule.navEntries) { + expect(typeof entry.id).toBe("string"); + expect(typeof entry.href).toBe("string"); + expect(typeof entry.label).toBe("string"); + expect(typeof entry.icon).toBe("string"); + } + }); + + it("has a context contributor with id 'competitor-recent-changes'", async () => { + const { competitorDomainModule } = await import("../competitor"); + expect( + competitorDomainModule.contextContributors.map((c) => c.id), + ).toContain("competitor-recent-changes"); + }); + + it("has no handlers in competitorDomainModule.handlers for non-existent tools", async () => { + const { competitorDomainModule } = await import("../competitor"); + // handlers keys must be exactly the two tool names + expect(Object.keys(competitorDomainModule.handlers).sort()).toEqual([ + "list_competitor_changes", + "list_competitors", + ]); + }); +}); diff --git a/apps/web/src/lib/domains/competitor.ts b/apps/web/src/lib/domains/competitor.ts new file mode 100644 index 00000000..7ad5a0db --- /dev/null +++ b/apps/web/src/lib/domains/competitor.ts @@ -0,0 +1,74 @@ +/** + * Competitor domain module (Task 10). + * + * Non-core domain: gated by the per-company aiFeatureFlags.features["competitor"] + * toggle (default on). No deployment capability key required — mirrors company-knowledge. + * + * core:false — no `capability` field set; isDomainEnabled falls through to the + * per-company flag with a default-on behaviour, consistent with company-knowledge. + * + * mcpExclude omitted → list_competitors / list_competitor_changes are MCP-exposed too + * (both are read-only and safe to surface via MCP). + * + * NOTE: the financial naming guard (tools-naming.test.ts) only iterates + * getFinancialTools(), so these list_* tools are not covered by it. If that guard + * is ever extended to domain tools, add list_competitors / list_competitor_changes + * to CONTROL_TOOLS (same bucket as list_accounts). + */ + +import type { + ContextContributor, + ContextSection, + ContributeCtx, +} from "@burnless/ai"; +import { listChanges } from "@burnless/db"; +import { competitorTools, competitorHandlers } from "@/lib/ai-tools/competitor"; +import type { DomainModule, DomainNavEntry } from "./contracts"; + +const DOMAIN = "competitor"; + +// ── Context contributor ──────────────────────────────────────────────────────── + +export const competitorContributor: ContextContributor = { + id: "competitor-recent-changes", + domain: DOMAIN, + async sections(ctx: ContributeCtx): Promise { + try { + const rows = await listChanges(ctx.companyId, { limit: 10 }); + if (rows.length === 0) return []; + const body = rows + .map((r) => `- ${r.summary}`) + .join("\n"); + return [{ heading: "Recent competitor changes", body, order: 40 }]; + } catch { + // Graceful degradation: a read failure must never break the turn. + return []; + } + }, +}; + +// ── Nav entries ──────────────────────────────────────────────────────────────── + +/** Backend nav entry: icon as Lucide component name string (sidebar maps to component). */ +export const competitorNavEntries: DomainNavEntry[] = [ + { + id: "competitors", + href: "/competitors", + label: "Competitors", + icon: "Swords", + }, +]; + +// ── Domain module ────────────────────────────────────────────────────────────── + +export const competitorDomainModule: DomainModule = { + id: DOMAIN, + // non-core: gated by per-company toggle (aiFeatureFlags.features["competitor"]), + // default on. No `capability` key — same pattern as company-knowledge. + tools: competitorTools, // ← surfaces list_competitors + list_competitor_changes to the LLM + handlers: competitorHandlers, + contextContributors: [competitorContributor], + promptSections: [], + navEntries: competitorNavEntries, + // mcpExclude omitted → both read tools are exposed over MCP. +}; diff --git a/apps/web/src/lib/domains/index.ts b/apps/web/src/lib/domains/index.ts index 012b2fff..b45311b5 100644 --- a/apps/web/src/lib/domains/index.ts +++ b/apps/web/src/lib/domains/index.ts @@ -15,6 +15,7 @@ import { companyKnowledgeModule } from "./company-knowledge"; import { memoryDomainModule } from "./memory"; import { skillsDomainModule } from "./skills"; import { integrationsDomainModule } from "./integrations"; +import { competitorDomainModule } from "./competitor"; let registered = false; @@ -26,6 +27,7 @@ export function registerDomains(): void { domainRegistry.register(memoryDomainModule); domainRegistry.register(skillsDomainModule); domainRegistry.register(integrationsDomainModule); + domainRegistry.register(competitorDomainModule); } // Auto-register at module load so any importer gets a populated registry. diff --git a/apps/web/src/lib/mutation-bus.ts b/apps/web/src/lib/mutation-bus.ts index 4507a7cc..94e874ea 100644 --- a/apps/web/src/lib/mutation-bus.ts +++ b/apps/web/src/lib/mutation-bus.ts @@ -31,6 +31,13 @@ export const FINANCIAL_DOMAINS = new Set([ "scenario", ]); +/** + * Non-financial domains that are still published on the bus so their SWR hooks + * can live-update (same-tab AND cross-tab). Kept separate from FINANCIAL_DOMAINS + * so competitor syncs do NOT reset the AI-insight stale countdown. + */ +export const COMPETITOR_DOMAINS = new Set(["competitor"]); + type Handler = (e: MutationEvent) => void; const handlers = new Set(); let storageBound = false; @@ -83,6 +90,7 @@ export function domainFromUrl(url: string): string { if (url.includes("/revenue-streams")) return "revenue"; if (url.includes("/funding-rounds")) return "funding"; if (url.includes("/scenarios")) return "scenario"; + if (url.includes("/competitors")) return "competitor"; // Non-financial (insights regen POST, chat, ai-config, preferences, …) → "other", // which apiFetch does NOT emit. Critically this keeps the insights regen POST out of // the bus, so auto-regen can't retrigger itself into an infinite loop. diff --git a/apps/web/src/lib/scheduler/__tests__/system-jobs.test.ts b/apps/web/src/lib/scheduler/__tests__/system-jobs.test.ts index db9f077f..822fa32c 100644 --- a/apps/web/src/lib/scheduler/__tests__/system-jobs.test.ts +++ b/apps/web/src/lib/scheduler/__tests__/system-jobs.test.ts @@ -18,6 +18,9 @@ vi.mock("@/lib/cron/batch-regenerate", () => ({ vi.mock("@/lib/integrations/run-all-syncs", () => ({ runAllIntegrationSyncs: vi.fn().mockResolvedValue({ synced: 0, failed: 0 }), })); +vi.mock("@/lib/competitor/pipeline", () => ({ + runDueCompetitorSyncs: vi.fn().mockResolvedValue({ ok: true, summary: "0 synced, 0 skipped, 0 failed" }), +})); describe("SYSTEM_JOBS registry", () => { it("has unique ids", () => { @@ -73,3 +76,14 @@ describe("integration-sync system job", () => { expect(result.summary).toContain("Synced"); }); }); + +describe("competitor-sync system job", () => { + it("is registered with the hourly schedule and its run() resolves ok", async () => { + const job = SYSTEM_JOBS.find((j) => j.id === "competitor-sync"); + expect(job).toBeDefined(); + expect(job!.schedule).toBe("0 * * * *"); + const result = await job!.run(); + expect(result.ok).toBe(true); + expect(result.summary).toContain("synced"); + }); +}); diff --git a/apps/web/src/lib/scheduler/system-jobs.ts b/apps/web/src/lib/scheduler/system-jobs.ts index 6cfdbed7..fb678c76 100644 --- a/apps/web/src/lib/scheduler/system-jobs.ts +++ b/apps/web/src/lib/scheduler/system-jobs.ts @@ -4,6 +4,7 @@ import { cleanupExpiredData } from "@/lib/data-retention"; import { runWeeklyDigest } from "@/lib/cron/weekly-digest"; import { runBatchRegenerate } from "@/lib/cron/batch-regenerate"; import { runAllIntegrationSyncs } from "@/lib/integrations/run-all-syncs"; +import { runDueCompetitorSyncs } from "@/lib/competitor/pipeline"; /** * Operational jobs registered in code (NOT in the scheduledJobs table). The @@ -46,4 +47,12 @@ export const SYSTEM_JOBS: SystemJob[] = [ return { ok: true, summary: `Synced ${r.synced} integration(s), ${r.failed} failed` }; }, }, + { + id: "competitor-sync", + schedule: "0 * * * *", // hourly tick; each source runs only when its intervalHours is due + run: async () => { + const r = await runDueCompetitorSyncs(new Date()); + return { ok: r.ok, summary: r.summary }; + }, + }, ]; diff --git a/apps/web/src/lib/swr/competitor.ts b/apps/web/src/lib/swr/competitor.ts new file mode 100644 index 00000000..d3340da3 --- /dev/null +++ b/apps/web/src/lib/swr/competitor.ts @@ -0,0 +1,103 @@ +"use client"; + +/** + * SWR hooks for the competitor-analysis domain. + * + * Live-update mechanics mirror useTransactions (WS2 lesson): both hooks + * subscribe to the mutation bus and call mutate() when a competitor-domain + * event fires (same-tab AND cross-tab). The competitor domain is kept separate + * from FINANCIAL_DOMAINS so competitor syncs do NOT reset the AI-insight stale + * countdown — only the SWR client cache is refreshed. + */ + +import { useEffect } from "react"; +import useSWR, { type SWRConfiguration } from "swr"; +import { KEYS } from "./keys"; +import { subscribeMutation, COMPETITOR_DOMAINS } from "@/lib/mutation-bus"; + +// ── DTO types (JSON-safe: Date → ISO string) ───────────────────────────────── + +/** A competitor row as returned by GET /api/competitors (JSON: Date → ISO string). */ +export interface CompetitorDto { + id: string; + companyId: string; + name: string; + url: string; + /** "active" | "paused" */ + status: string; + createdAt: string; + updatedAt: string; +} + +/** A competitor change row as returned by GET /api/competitors/[id]/changes. */ +export interface CompetitorChangeDto { + id: string; + competitorId: string; + sourceId: string; + snapshotId: string; + companyId: string; + detectedAt: string; + changeType: string; + summary: string; + before: Record | null; + after: Record | null; + /** "info" | "warning" | "critical" */ + severity: string; + acknowledgedAt: string | null; + createdAt: string; +} + +/** Payload shape returned by GET /api/competitors. */ +export interface CompetitorsPayload { + competitors: CompetitorDto[]; +} + +/** Payload shape returned by GET /api/competitors/[id]/changes. */ +export interface CompetitorChangesPayload { + changes: CompetitorChangeDto[]; +} + +// ── Hooks ───────────────────────────────────────────────────────────────────── + +/** + * All competitors for the current company. + * + * Live-updates after add/delete/sync via the competitor mutation bus — any + * successful POST/PATCH/DELETE to /api/competitors* triggers a refetch here + * (same-tab AND cross-tab), mirroring the useTransactions pattern from WS2. + */ +export function useCompetitors( + config?: SWRConfiguration, +) { + const swr = useSWR(KEYS.competitors, { ...config }); + const { mutate } = swr; + useEffect(() => { + return subscribeMutation((e) => { + if (COMPETITOR_DOMAINS.has(e.domain)) mutate(); + }); + }, [mutate]); + return swr; +} + +/** + * Detected changes for a single competitor, newest-first. + * + * Pass `competitorId` to enable the fetch; omit (or pass undefined/null) to + * suspend fetching (SWR null-key pattern). Live-updates on competitor-domain + * mutations so the changes list refreshes after a sync completes. + */ +export function useCompetitorChanges( + competitorId?: string | null, + config?: SWRConfiguration, +) { + const key = competitorId ? KEYS.competitorChanges(competitorId) : null; + const swr = useSWR(key, { ...config }); + const { mutate } = swr; + useEffect(() => { + if (!competitorId) return; + return subscribeMutation((e) => { + if (COMPETITOR_DOMAINS.has(e.domain)) mutate(); + }); + }, [competitorId, mutate]); + return swr; +} diff --git a/apps/web/src/lib/swr/index.ts b/apps/web/src/lib/swr/index.ts index 8857e8ea..b305d4b6 100644 --- a/apps/web/src/lib/swr/index.ts +++ b/apps/web/src/lib/swr/index.ts @@ -7,6 +7,16 @@ export { SWRProvider } from "./provider"; export { KEYS } from "./keys"; +export { + useCompetitors, + useCompetitorChanges, +} from "./competitor"; +export type { + CompetitorDto, + CompetitorChangeDto, + CompetitorsPayload, + CompetitorChangesPayload, +} from "./competitor"; export { fetcher, FetchError } from "./fetcher"; export { useScenarios, diff --git a/apps/web/src/lib/swr/keys.ts b/apps/web/src/lib/swr/keys.ts index 7e0a284a..623523a1 100644 --- a/apps/web/src/lib/swr/keys.ts +++ b/apps/web/src/lib/swr/keys.ts @@ -72,6 +72,10 @@ export const KEYS = { sessionDisabledTools: (conversationId: string) => `/api/chat/session-tools?conversationId=${conversationId}`, + // Competitor analysis domain + competitors: "/api/competitors", + competitorChanges: (id: string) => `/api/competitors/${id}/changes`, + // AI providers manager (Settings → AI Providers, #49 P3) aiProviders: "/api/ai-features/providers", aiProvider: (id: string) => `/api/ai-features/providers/${id}`, diff --git a/packages/db/drizzle/0015_shocking_lord_tyger.sql b/packages/db/drizzle/0015_shocking_lord_tyger.sql new file mode 100644 index 00000000..8c127065 --- /dev/null +++ b/packages/db/drizzle/0015_shocking_lord_tyger.sql @@ -0,0 +1,71 @@ +CREATE TABLE "competitor_changes" ( + "id" text PRIMARY KEY NOT NULL, + "competitor_id" text NOT NULL, + "source_id" text NOT NULL, + "snapshot_id" text NOT NULL, + "company_id" text NOT NULL, + "detected_at" timestamp DEFAULT now() NOT NULL, + "change_type" text NOT NULL, + "summary" text NOT NULL, + "before" jsonb, + "after" jsonb, + "severity" text DEFAULT 'info' NOT NULL, + "acknowledged_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "competitor_snapshots" ( + "id" text PRIMARY KEY NOT NULL, + "competitor_id" text NOT NULL, + "source_id" text NOT NULL, + "company_id" text NOT NULL, + "captured_at" timestamp DEFAULT now() NOT NULL, + "raw" text NOT NULL, + "raw_hash" text NOT NULL, + "structured" jsonb NOT NULL, + "structured_hash" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "competitor_sources" ( + "id" text PRIMARY KEY NOT NULL, + "competitor_id" text NOT NULL, + "company_id" text NOT NULL, + "type" text NOT NULL, + "url" text NOT NULL, + "config" jsonb, + "enabled" boolean DEFAULT true NOT NULL, + "interval_hours" integer DEFAULT 168 NOT NULL, + "last_run_at" timestamp, + "last_status" text, + "health_state" text DEFAULT 'ok' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "competitors" ( + "id" text PRIMARY KEY NOT NULL, + "company_id" text NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "competitor_changes" ADD CONSTRAINT "competitor_changes_competitor_id_competitors_id_fk" FOREIGN KEY ("competitor_id") REFERENCES "public"."competitors"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_changes" ADD CONSTRAINT "competitor_changes_source_id_competitor_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."competitor_sources"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_changes" ADD CONSTRAINT "competitor_changes_snapshot_id_competitor_snapshots_id_fk" FOREIGN KEY ("snapshot_id") REFERENCES "public"."competitor_snapshots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_changes" ADD CONSTRAINT "competitor_changes_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_snapshots" ADD CONSTRAINT "competitor_snapshots_competitor_id_competitors_id_fk" FOREIGN KEY ("competitor_id") REFERENCES "public"."competitors"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_snapshots" ADD CONSTRAINT "competitor_snapshots_source_id_competitor_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."competitor_sources"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_snapshots" ADD CONSTRAINT "competitor_snapshots_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_sources" ADD CONSTRAINT "competitor_sources_competitor_id_competitors_id_fk" FOREIGN KEY ("competitor_id") REFERENCES "public"."competitors"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitor_sources" ADD CONSTRAINT "competitor_sources_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "competitors" ADD CONSTRAINT "competitors_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "competitor_changes_company_idx" ON "competitor_changes" USING btree ("company_id","detected_at");--> statement-breakpoint +CREATE INDEX "competitor_changes_competitor_idx" ON "competitor_changes" USING btree ("competitor_id");--> statement-breakpoint +CREATE INDEX "competitor_snapshots_source_idx" ON "competitor_snapshots" USING btree ("source_id","captured_at");--> statement-breakpoint +CREATE INDEX "competitor_sources_company_idx" ON "competitor_sources" USING btree ("company_id");--> statement-breakpoint +CREATE INDEX "competitor_sources_competitor_idx" ON "competitor_sources" USING btree ("competitor_id");--> statement-breakpoint +CREATE INDEX "competitors_company_idx" ON "competitors" USING btree ("company_id"); \ No newline at end of file diff --git a/packages/db/drizzle/0016_sad_moondragon.sql b/packages/db/drizzle/0016_sad_moondragon.sql new file mode 100644 index 00000000..916afc69 --- /dev/null +++ b/packages/db/drizzle/0016_sad_moondragon.sql @@ -0,0 +1,2 @@ +ALTER TABLE "competitor_snapshots" ADD COLUMN "normalized" text;--> statement-breakpoint +ALTER TABLE "competitor_snapshots" ADD COLUMN "normalized_hash" text; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0015_snapshot.json b/packages/db/drizzle/meta/0015_snapshot.json new file mode 100644 index 00000000..e1f77ee9 --- /dev/null +++ b/packages/db/drizzle/meta/0015_snapshot.json @@ -0,0 +1,8816 @@ +{ + "id": "b5af6d6c-18e5-41c8-a653-6af8fa6c358a", + "prevId": "ba233426-f52e-4c4f-9a20-e14d97e5dc5a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "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 + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "accounts_user_idx": { + "name": "accounts_user_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": { + "accounts_provider_provider_account_id_pk": { + "name": "accounts_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_user_idx": { + "name": "sessions_user_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": {}, + "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": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "two_factor_backup_codes": { + "name": "two_factor_backup_codes", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_four": { + "name": "last_four", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "api_tokens_hash_idx": { + "name": "api_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_user_company_idx": { + "name": "api_tokens_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_company_idx": { + "name": "api_tokens_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_tokens_company_id_companies_id_fk": { + "name": "api_tokens_company_id_companies_id_fk", + "tableFrom": "api_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "company_stage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_seed'" + }, + "business_model": { + "name": "business_model", + "type": "business_model", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'saas'" + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "founded_date": { + "name": "founded_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "fiscal_year_end": { + "name": "fiscal_year_end", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 12 + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en-US'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'America/New_York'" + }, + "region": { + "name": "region", + "type": "data_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'us-east'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_provider": { + "name": "billing_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_subscription_id": { + "name": "billing_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_plan": { + "name": "billing_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'free'" + }, + "benefits_rates": { + "name": "benefits_rates", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "founders_ownership_percent": { + "name": "founders_ownership_percent", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": true, + "default": "'100.0000'" + }, + "mcp_server_enabled": { + "name": "mcp_server_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": {}, + "foreignKeys": { + "companies_owner_id_users_id_fk": { + "name": "companies_owner_id_users_id_fk", + "tableFrom": "companies", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_members": { + "name": "company_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "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": { + "company_member_unique": { + "name": "company_member_unique", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_member_user_idx": { + "name": "company_member_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_members_company_id_companies_id_fk": { + "name": "company_members_company_id_companies_id_fk", + "tableFrom": "company_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_members_user_id_users_id_fk": { + "name": "company_members_user_id_users_id_fk", + "tableFrom": "company_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_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": { + "departments_company_idx": { + "name": "departments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_company_id_companies_id_fk": { + "name": "departments_company_id_companies_id_fk", + "tableFrom": "departments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_code_redemptions": { + "name": "invite_code_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invite_code_id": { + "name": "invite_code_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_redemptions_code_idx": { + "name": "invite_redemptions_code_idx", + "columns": [ + { + "expression": "invite_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_redemptions_user_code_idx": { + "name": "invite_redemptions_user_code_idx", + "columns": [ + { + "expression": "invite_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_code_redemptions_invite_code_id_invite_codes_id_fk": { + "name": "invite_code_redemptions_invite_code_id_invite_codes_id_fk", + "tableFrom": "invite_code_redemptions", + "tableTo": "invite_codes", + "columnsFrom": [ + "invite_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invite_code_redemptions_user_id_users_id_fk": { + "name": "invite_code_redemptions_user_id_users_id_fk", + "tableFrom": "invite_code_redemptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "invite_code_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'single_use'" + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "current_redemptions": { + "name": "current_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "free_platform_days": { + "name": "free_platform_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "ai_credits_cents": { + "name": "ai_credits_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "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": { + "invite_codes_code_idx": { + "name": "invite_codes_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_codes_created_by_idx": { + "name": "invite_codes_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_codes_active_idx": { + "name": "invite_codes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_codes_created_by_users_id_fk": { + "name": "invite_codes_created_by_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_auth_codes": { + "name": "oauth_auth_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_auth_codes_hash_idx": { + "name": "oauth_auth_codes_hash_idx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_auth_codes_client_id_oauth_clients_id_fk": { + "name": "oauth_auth_codes_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_auth_codes_user_id_users_id_fk": { + "name": "oauth_auth_codes_user_id_users_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_auth_codes_company_id_companies_id_fk": { + "name": "oauth_auth_codes_company_id_companies_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_hash": { + "name": "refresh_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "oauth_tokens_access_hash_idx": { + "name": "oauth_tokens_access_hash_idx", + "columns": [ + { + "expression": "access_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_refresh_hash_idx": { + "name": "oauth_tokens_refresh_hash_idx", + "columns": [ + { + "expression": "refresh_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_grant_idx": { + "name": "oauth_tokens_grant_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_user_company_idx": { + "name": "oauth_tokens_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_client_id_oauth_clients_id_fk": { + "name": "oauth_tokens_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_user_id_users_id_fk": { + "name": "oauth_tokens_user_id_users_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_company_id_companies_id_fk": { + "name": "oauth_tokens_company_id_companies_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_credentials": { + "name": "integration_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "integration_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "livemode": { + "name": "livemode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "integration_credentials_company_type_idx": { + "name": "integration_credentials_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_credentials_company_id_companies_id_fk": { + "name": "integration_credentials_company_id_companies_id_fk", + "tableFrom": "integration_credentials", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "integration_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "integrations_company_type_idx": { + "name": "integrations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrations_company_id_companies_id_fk": { + "name": "integrations_company_id_companies_id_fk", + "tableFrom": "integrations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_connections": { + "name": "mcp_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "mcp_owner_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "mcp_transport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "mcp_auth_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "status": { + "name": "status", + "type": "mcp_connection_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_connected_at": { + "name": "last_connected_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": { + "mcp_connections_company_idx": { + "name": "mcp_connections_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_owner_idx": { + "name": "mcp_connections_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_company_name_idx": { + "name": "mcp_connections_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_company_slug_idx": { + "name": "mcp_connections_company_slug_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_connections_company_id_companies_id_fk": { + "name": "mcp_connections_company_id_companies_id_fk", + "tableFrom": "mcp_connections", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_connections_owner_user_id_users_id_fk": { + "name": "mcp_connections_owner_user_id_users_id_fk", + "tableFrom": "mcp_connections", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_connections_personal_owner_check": { + "name": "mcp_connections_personal_owner_check", + "value": "(owner_scope = 'personal') = (owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_credentials": { + "name": "mcp_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "mcp_auth_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_registration": { + "name": "client_registration", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_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": { + "mcp_credentials_connection_idx": { + "name": "mcp_credentials_connection_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_credentials_mcp_connection_id_mcp_connections_id_fk": { + "name": "mcp_credentials_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "mcp_credentials", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tool_prefs": { + "name": "mcp_tool_prefs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "perm_class_override": { + "name": "perm_class_override", + "type": "mcp_tool_perm", + "typeSchema": "public", + "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": { + "mcp_tool_prefs_connection_tool_idx": { + "name": "mcp_tool_prefs_connection_tool_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_tool_prefs_mcp_connection_id_mcp_connections_id_fk": { + "name": "mcp_tool_prefs_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "mcp_tool_prefs", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_preferences": { + "name": "dashboard_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "dashboard_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dynamic'" + }, + "hero_cards": { + "name": "hero_cards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "secondary_metrics": { + "name": "secondary_metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "card_mode_overrides": { + "name": "card_mode_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "card_scenario_overrides": { + "name": "card_scenario_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "custom_slug_overrides": { + "name": "custom_slug_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "slot_overrides": { + "name": "slot_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_metrics": { + "name": "custom_metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "closed_widgets": { + "name": "closed_widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "page_layouts": { + "name": "page_layouts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "dashboard_prefs_user_company_idx": { + "name": "dashboard_prefs_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_preferences_user_id_users_id_fk": { + "name": "dashboard_preferences_user_id_users_id_fk", + "tableFrom": "dashboard_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_preferences_company_id_companies_id_fk": { + "name": "dashboard_preferences_company_id_companies_id_fk", + "tableFrom": "dashboard_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.export_logs": { + "name": "export_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "export_type": { + "name": "export_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "export_logs_company_idx": { + "name": "export_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "export_logs_company_created_idx": { + "name": "export_logs_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "export_logs_user_idx": { + "name": "export_logs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "export_logs_company_id_companies_id_fk": { + "name": "export_logs_company_id_companies_id_fk", + "tableFrom": "export_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "export_logs_user_id_users_id_fk": { + "name": "export_logs_user_id_users_id_fk", + "tableFrom": "export_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "notification_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_unread_idx": { + "name": "notifications_unread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_company_id_companies_id_fk": { + "name": "notifications_company_id_companies_id_fk", + "tableFrom": "notifications", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.privacy_consents": { + "name": "privacy_consents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "consent_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "granted": { + "name": "granted", + "type": "boolean", + "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 + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "privacy_consents_user_idx": { + "name": "privacy_consents_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "privacy_consents_user_purpose_idx": { + "name": "privacy_consents_user_purpose_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purpose", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "privacy_consents_user_id_users_id_fk": { + "name": "privacy_consents_user_id_users_id_fk", + "tableFrom": "privacy_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_job_runs": { + "name": "scheduled_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scheduled_job_id": { + "name": "scheduled_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scheduled_job_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "scheduled_job_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_used": { + "name": "tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scheduled_job_runs_job_idx": { + "name": "scheduled_job_runs_job_idx", + "columns": [ + { + "expression": "scheduled_job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_job_runs_company_idx": { + "name": "scheduled_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_job_runs_scheduled_job_id_scheduled_jobs_id_fk": { + "name": "scheduled_job_runs_scheduled_job_id_scheduled_jobs_id_fk", + "tableFrom": "scheduled_job_runs", + "tableTo": "scheduled_jobs", + "columnsFrom": [ + "scheduled_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduled_job_runs_company_id_companies_id_fk": { + "name": "scheduled_job_runs_company_id_companies_id_fk", + "tableFrom": "scheduled_job_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_jobs": { + "name": "scheduled_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_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 + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_kind": { + "name": "action_kind", + "type": "scheduled_job_action_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bound_connection_ids": { + "name": "bound_connection_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "scheduled_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "notify_policy": { + "name": "notify_policy", + "type": "scheduled_job_notify_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'smart'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_run_cursor": { + "name": "last_run_cursor", + "type": "jsonb", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scheduled_jobs_company_idx": { + "name": "scheduled_jobs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_jobs_due_idx": { + "name": "scheduled_jobs_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_jobs_company_id_companies_id_fk": { + "name": "scheduled_jobs_company_id_companies_id_fk", + "tableFrom": "scheduled_jobs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduled_jobs_created_by_user_id_users_id_fk": { + "name": "scheduled_jobs_created_by_user_id_users_id_fk", + "tableFrom": "scheduled_jobs", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidebar_order": { + "name": "sidebar_order", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "quick_action_mode": { + "name": "quick_action_mode", + "type": "quick_action_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dynamic'" + }, + "quick_action_mode_overrides": { + "name": "quick_action_mode_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_quick_actions": { + "name": "custom_quick_actions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sidebar_collapsed": { + "name": "sidebar_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled_mcp_connections": { + "name": "disabled_mcp_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled_builtin_tools": { + "name": "disabled_builtin_tools", + "type": "jsonb", + "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": { + "user_preferences_user_company_idx": { + "name": "user_preferences_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_company_id_companies_id_fk": { + "name": "user_preferences_company_id_companies_id_fk", + "tableFrom": "user_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_digests": { + "name": "weekly_digests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "week_start": { + "name": "week_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "narrative": { + "name": "narrative", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deterministic_summary": { + "name": "deterministic_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_sent_at": { + "name": "email_sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "weekly_digests_company_idx": { + "name": "weekly_digests_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "weekly_digests_company_week_idx": { + "name": "weekly_digests_company_week_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weekly_digests_company_id_companies_id_fk": { + "name": "weekly_digests_company_id_companies_id_fk", + "tableFrom": "weekly_digests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_grants": { + "name": "session_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "session_disabled_tools": { + "name": "session_disabled_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "ai_conversations_company_idx": { + "name": "ai_conversations_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_conversations_company_user_idx": { + "name": "ai_conversations_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_company_id_companies_id_fk": { + "name": "ai_conversations_company_id_companies_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_feature_flags": { + "name": "ai_feature_flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "master_enabled": { + "name": "master_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "data_mode": { + "name": "data_mode", + "type": "ai_data_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "monthly_budget_cents": { + "name": "monthly_budget_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"onboarding\":true,\"chat\":true,\"insights\":true,\"uiPersonalization\":true,\"autoCategorization\":true,\"weeklyDigest\":true}'::jsonb" + }, + "write_mode": { + "name": "write_mode", + "type": "ai_write_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'confirm'" + }, + "companion_name": { + "name": "companion_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Companion'" + }, + "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": { + "ai_feature_flags_company_idx": { + "name": "ai_feature_flags_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_feature_flags_company_id_companies_id_fk": { + "name": "ai_feature_flags_company_id_companies_id_fk", + "tableFrom": "ai_feature_flags", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_insight_cache": { + "name": "ai_insight_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ai_insight_cache_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stale_at": { + "name": "stale_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stale_reason": { + "name": "stale_reason", + "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": { + "ai_insight_cache_company_idx": { + "name": "ai_insight_cache_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_insight_cache_company_key_idx": { + "name": "ai_insight_cache_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_insight_cache_company_id_companies_id_fk": { + "name": "ai_insight_cache_company_id_companies_id_fk", + "tableFrom": "ai_insight_cache", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_permission_defaults": { + "name": "ai_permission_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_mode": { + "name": "read_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "write_mode": { + "name": "write_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "delete_mode": { + "name": "delete_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "web_search_mode": { + "name": "web_search_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "browser_use_mode": { + "name": "browser_use_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "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": { + "ai_permission_defaults_user_company_idx": { + "name": "ai_permission_defaults_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_permission_defaults_user_id_users_id_fk": { + "name": "ai_permission_defaults_user_id_users_id_fk", + "tableFrom": "ai_permission_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_permission_defaults_company_id_companies_id_fk": { + "name": "ai_permission_defaults_company_id_companies_id_fk", + "tableFrom": "ai_permission_defaults", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_provider_models": { + "name": "ai_provider_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supports_tools": { + "name": "supports_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "supports_images": { + "name": "supports_images", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "ai_provider_model_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_provider_models_provider_idx": { + "name": "ai_provider_models_provider_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_provider_models_provider_model_idx": { + "name": "ai_provider_models_provider_model_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_provider_models_provider_id_ai_providers_id_fk": { + "name": "ai_provider_models_provider_id_ai_providers_id_fk", + "tableFrom": "ai_provider_models", + "tableTo": "ai_providers", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "ai_provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_mode": { + "name": "api_key_mode", + "type": "ai_api_key_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user_provided'" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "drop_params": { + "name": "drop_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "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": { + "ai_providers_company_idx": { + "name": "ai_providers_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_company_id_companies_id_fk": { + "name": "ai_providers_company_id_companies_id_fk", + "tableFrom": "ai_providers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_tool_audit_logs": { + "name": "ai_tool_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_job_run_id": { + "name": "scheduled_job_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ai_tool_audit_log_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "permission_decision": { + "name": "permission_decision", + "type": "ai_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_info": { + "name": "client_info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_tool_audit_company_idx": { + "name": "ai_tool_audit_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_user_idx": { + "name": "ai_tool_audit_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_created_idx": { + "name": "ai_tool_audit_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_tool_idx": { + "name": "ai_tool_audit_tool_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_conversation_idx": { + "name": "ai_tool_audit_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_mcp_connection_idx": { + "name": "ai_tool_audit_mcp_connection_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_scheduled_job_run_idx": { + "name": "ai_tool_audit_scheduled_job_run_idx", + "columns": [ + { + "expression": "scheduled_job_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_tool_audit_logs_company_id_companies_id_fk": { + "name": "ai_tool_audit_logs_company_id_companies_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_user_id_users_id_fk": { + "name": "ai_tool_audit_logs_user_id_users_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_conversation_id_ai_conversations_id_fk": { + "name": "ai_tool_audit_logs_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_mcp_connection_id_mcp_connections_id_fk": { + "name": "ai_tool_audit_logs_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_scheduled_job_run_id_scheduled_job_runs_id_fk": { + "name": "ai_tool_audit_logs_scheduled_job_run_id_scheduled_job_runs_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "scheduled_job_runs", + "columnsFrom": [ + "scheduled_job_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_turn_events": { + "name": "ai_turn_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ai_turn_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_turn_events_conversation_seq_idx": { + "name": "ai_turn_events_conversation_seq_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_turn_events_open_gate_idx": { + "name": "ai_turn_events_open_gate_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"ai_turn_events\".\"type\" = 'gate' AND \"ai_turn_events\".\"resolved_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_turn_events_conversation_id_ai_conversations_id_fk": { + "name": "ai_turn_events_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_turn_events", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_logs": { + "name": "ai_usage_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "estimated_cost_micros": { + "name": "estimated_cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_company_idx": { + "name": "ai_usage_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_feature_idx": { + "name": "ai_usage_feature_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_created_idx": { + "name": "ai_usage_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_logs_company_id_companies_id_fk": { + "name": "ai_usage_logs_company_id_companies_id_fk", + "tableFrom": "ai_usage_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insight_invalidations": { + "name": "insight_invalidations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "insight_type": { + "name": "insight_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mutation_source": { + "name": "mutation_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_invalidated_at": { + "name": "first_invalidated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_mutation_at": { + "name": "last_mutation_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "insight_invalidations_company_type_idx": { + "name": "insight_invalidations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "insight_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insight_invalidations_pending_idx": { + "name": "insight_invalidations_pending_idx", + "columns": [ + { + "expression": "processed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insight_invalidations_company_id_companies_id_fk": { + "name": "insight_invalidations_company_id_companies_id_fk", + "tableFrom": "insight_invalidations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_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": { + "memory_company_idx": { + "name": "memory_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_company_domain_kind_idx": { + "name": "memory_company_domain_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_company_tier_idx": { + "name": "memory_company_tier_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_embedding_hnsw": { + "name": "memory_embedding_hnsw", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "where": "\"memory\".\"embedding\" IS NOT NULL", + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "memory_company_id_companies_id_fk": { + "name": "memory_company_id_companies_id_fk", + "tableFrom": "memory", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_user_id_users_id_fk": { + "name": "memory_user_id_users_id_fk", + "tableFrom": "memory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bonuses": { + "name": "bonuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payout_month": { + "name": "payout_month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "bonus_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'performance'" + }, + "notes": { + "name": "notes", + "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": { + "bonuses_company_idx": { + "name": "bonuses_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bonuses_headcount_month_idx": { + "name": "bonuses_headcount_month_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payout_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bonuses_company_id_companies_id_fk": { + "name": "bonuses_company_id_companies_id_fk", + "tableFrom": "bonuses", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bonuses_headcount_id_headcount_plans_id_fk": { + "name": "bonuses_headcount_id_headcount_plans_id_fk", + "tableFrom": "bonuses", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.equity_grants": { + "name": "equity_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_date": { + "name": "grant_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "numeric(18, 4)", + "primaryKey": false, + "notNull": true + }, + "strike_price": { + "name": "strike_price", + "type": "numeric(18, 4)", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "equity_grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'iso'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "equity_grants_company_idx": { + "name": "equity_grants_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "equity_grants_headcount_idx": { + "name": "equity_grants_headcount_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "equity_grants_company_id_companies_id_fk": { + "name": "equity_grants_company_id_companies_id_fk", + "tableFrom": "equity_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "equity_grants_headcount_id_headcount_plans_id_fk": { + "name": "equity_grants_headcount_id_headcount_plans_id_fk", + "tableFrom": "equity_grants", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_accounts": { + "name": "financial_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "account_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "covers_headcount": { + "name": "covers_headcount", + "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": { + "financial_accounts_company_idx": { + "name": "financial_accounts_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_accounts_parent_idx": { + "name": "financial_accounts_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "financial_accounts_company_id_companies_id_fk": { + "name": "financial_accounts_company_id_companies_id_fk", + "tableFrom": "financial_accounts", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_audit_logs": { + "name": "financial_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "audit_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "financial_audit_company_idx": { + "name": "financial_audit_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_entity_idx": { + "name": "financial_audit_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_user_idx": { + "name": "financial_audit_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_created_idx": { + "name": "financial_audit_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "financial_audit_logs_company_id_companies_id_fk": { + "name": "financial_audit_logs_company_id_companies_id_fk", + "tableFrom": "financial_audit_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "financial_audit_logs_user_id_users_id_fk": { + "name": "financial_audit_logs_user_id_users_id_fk", + "tableFrom": "financial_audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forecast_lines": { + "name": "forecast_lines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "forecast_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "expense_frequency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + }, + "is_one_time": { + "name": "is_one_time", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recurring": { + "name": "is_recurring", + "type": "boolean", + "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": { + "forecast_lines_company_idx": { + "name": "forecast_lines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_account_idx": { + "name": "forecast_lines_company_account_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_department_idx": { + "name": "forecast_lines_company_department_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_vendor_idx": { + "name": "forecast_lines_vendor_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_name_idx": { + "name": "forecast_lines_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"forecast_lines\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forecast_lines_company_id_companies_id_fk": { + "name": "forecast_lines_company_id_companies_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forecast_lines_account_id_financial_accounts_id_fk": { + "name": "forecast_lines_account_id_financial_accounts_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forecast_lines_department_id_departments_id_fk": { + "name": "forecast_lines_department_id_departments_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forecast_values": { + "name": "forecast_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "forecast_line_id": { + "name": "forecast_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "is_override": { + "name": "is_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "forecast_values_line_idx": { + "name": "forecast_values_line_idx", + "columns": [ + { + "expression": "forecast_line_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_values_month_idx": { + "name": "forecast_values_month_idx", + "columns": [ + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_values_line_month_idx": { + "name": "forecast_values_line_month_idx", + "columns": [ + { + "expression": "forecast_line_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forecast_values_forecast_line_id_forecast_lines_id_fk": { + "name": "forecast_values_forecast_line_id_forecast_lines_id_fk", + "tableFrom": "forecast_values", + "tableTo": "forecast_lines", + "columnsFrom": [ + "forecast_line_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_round_investors": { + "name": "funding_round_investors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "funding_round_id": { + "name": "funding_round_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_invested": { + "name": "amount_invested", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "funding_round_investors_round_idx": { + "name": "funding_round_investors_round_idx", + "columns": [ + { + "expression": "funding_round_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "funding_round_investors_funding_round_id_funding_rounds_id_fk": { + "name": "funding_round_investors_funding_round_id_funding_rounds_id_fk", + "tableFrom": "funding_round_investors", + "tableTo": "funding_rounds", + "columnsFrom": [ + "funding_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_rounds": { + "name": "funding_rounds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "funding_round_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "pre_money_valuation": { + "name": "pre_money_valuation", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "dilution_percent": { + "name": "dilution_percent", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": false + }, + "is_projected": { + "name": "is_projected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "close_date": { + "name": "close_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "funding_rounds_company_idx": { + "name": "funding_rounds_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "funding_rounds_company_id_companies_id_fk": { + "name": "funding_rounds_company_id_companies_id_fk", + "tableFrom": "funding_rounds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.headcount_plans": { + "name": "headcount_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "employee_type": { + "name": "employee_type", + "type": "headcount_employee_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full_time'" + }, + "count": { + "name": "count", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'1.00'" + }, + "salary": { + "name": "salary", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "hourly_rate": { + "name": "hourly_rate", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "hours_per_week": { + "name": "hours_per_week", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "benefits_rate": { + "name": "benefits_rate", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0.20'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "headcount_plans_company_idx": { + "name": "headcount_plans_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "headcount_plans_department_idx": { + "name": "headcount_plans_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "headcount_plans_company_id_companies_id_fk": { + "name": "headcount_plans_company_id_companies_id_fk", + "tableFrom": "headcount_plans", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "headcount_plans_department_id_departments_id_fk": { + "name": "headcount_plans_department_id_departments_id_fk", + "tableFrom": "headcount_plans", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_batches": { + "name": "import_batches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_batch_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "total_rows": { + "name": "total_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "imported_count": { + "name": "imported_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "column_mapping": { + "name": "column_mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_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": { + "import_batches_company_idx": { + "name": "import_batches_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "import_batches_account_idx": { + "name": "import_batches_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_batches_company_id_companies_id_fk": { + "name": "import_batches_company_id_companies_id_fk", + "tableFrom": "import_batches", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "import_batches_account_id_financial_accounts_id_fk": { + "name": "import_batches_account_id_financial_accounts_id_fk", + "tableFrom": "import_batches", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.merchant_category_mappings": { + "name": "merchant_category_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merchant_pattern": { + "name": "merchant_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "account_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user_override'" + }, + "override_count": { + "name": "override_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": { + "merchant_mappings_company_idx": { + "name": "merchant_mappings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_pattern_idx": { + "name": "merchant_mappings_pattern_idx", + "columns": [ + { + "expression": "merchant_pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_account_idx": { + "name": "merchant_mappings_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_company_pattern_idx": { + "name": "merchant_mappings_company_pattern_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "merchant_pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "merchant_category_mappings_company_id_companies_id_fk": { + "name": "merchant_category_mappings_company_id_companies_id_fk", + "tableFrom": "merchant_category_mappings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "merchant_category_mappings_account_id_financial_accounts_id_fk": { + "name": "merchant_category_mappings_account_id_financial_accounts_id_fk", + "tableFrom": "merchant_category_mappings", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metrics": { + "name": "metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formula": { + "name": "formula", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "category": { + "name": "category", + "type": "metric_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'financial'" + }, + "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": { + "metrics_company_slug_idx": { + "name": "metrics_company_slug_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metrics_company_id_companies_id_fk": { + "name": "metrics_company_id_companies_id_fk", + "tableFrom": "metrics", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.option_pools": { + "name": "option_pools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_reserved": { + "name": "total_reserved", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true + }, + "refresh_date": { + "name": "refresh_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "option_pools_company_idx": { + "name": "option_pools_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "option_pools_company_id_companies_id_fk": { + "name": "option_pools_company_id_companies_id_fk", + "tableFrom": "option_pools", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revenue_streams": { + "name": "revenue_streams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "revenue_stream_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'subscription'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "end_date": { + "name": "end_date", + "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": { + "revenue_streams_company_idx": { + "name": "revenue_streams_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "revenue_streams_active_idx": { + "name": "revenue_streams_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revenue_streams_company_id_companies_id_fk": { + "name": "revenue_streams_company_id_companies_id_fk", + "tableFrom": "revenue_streams", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.salary_changes": { + "name": "salary_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_date": { + "name": "effective_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "new_salary": { + "name": "new_salary", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "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": { + "salary_changes_company_idx": { + "name": "salary_changes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "salary_changes_headcount_date_idx": { + "name": "salary_changes_headcount_date_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "salary_changes_company_id_companies_id_fk": { + "name": "salary_changes_company_id_companies_id_fk", + "tableFrom": "salary_changes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "salary_changes_headcount_id_headcount_plans_id_fk": { + "name": "salary_changes_headcount_id_headcount_plans_id_fk", + "tableFrom": "salary_changes", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario_overrides": { + "name": "scenario_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "scenario_override_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "original_data": { + "name": "original_data", + "type": "jsonb", + "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": { + "scenario_overrides_unique": { + "name": "scenario_overrides_unique", + "columns": [ + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scenario_overrides_scenario_type": { + "name": "scenario_overrides_scenario_type", + "columns": [ + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_overrides_scenario_id_scenarios_id_fk": { + "name": "scenario_overrides_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_overrides", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenarios": { + "name": "scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "scenario_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'blank'" + }, + "status": { + "name": "status", + "type": "scenario_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_scenario_id": { + "name": "source_scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_conversation_id": { + "name": "ai_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auto_delete_at": { + "name": "auto_delete_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "scenarios_company_idx": { + "name": "scenarios_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenarios_company_id_companies_id_fk": { + "name": "scenarios_company_id_companies_id_fk", + "tableFrom": "scenarios", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_classes": { + "name": "share_classes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "class_type": { + "name": "class_type", + "type": "share_class_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "total_authorized": { + "name": "total_authorized", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true + }, + "total_issued": { + "name": "total_issued", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "par_value": { + "name": "par_value", + "type": "numeric(18, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0.000001'" + }, + "liquidation_preference": { + "name": "liquidation_preference", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "share_classes_company_idx": { + "name": "share_classes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_classes_company_id_companies_id_fk": { + "name": "share_classes_company_id_companies_id_fk", + "tableFrom": "share_classes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "transaction_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_batch_id": { + "name": "import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "transactions_company_date_idx": { + "name": "transactions_company_date_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_account_idx": { + "name": "transactions_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_external_id_idx": { + "name": "transactions_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_batch_idx": { + "name": "transactions_batch_idx", + "columns": [ + { + "expression": "import_batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_company_id_companies_id_fk": { + "name": "transactions_company_id_companies_id_fk", + "tableFrom": "transactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transactions_account_id_financial_accounts_id_fk": { + "name": "transactions_account_id_financial_accounts_id_fk", + "tableFrom": "transactions", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_changes": { + "name": "competitor_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before": { + "name": "before", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after": { + "name": "after", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "competitor_changes_company_idx": { + "name": "competitor_changes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "detected_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "competitor_changes_competitor_idx": { + "name": "competitor_changes_competitor_idx", + "columns": [ + { + "expression": "competitor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_changes_competitor_id_competitors_id_fk": { + "name": "competitor_changes_competitor_id_competitors_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_source_id_competitor_sources_id_fk": { + "name": "competitor_changes_source_id_competitor_sources_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitor_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_snapshot_id_competitor_snapshots_id_fk": { + "name": "competitor_changes_snapshot_id_competitor_snapshots_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitor_snapshots", + "columnsFrom": [ + "snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_company_id_companies_id_fk": { + "name": "competitor_changes_company_id_companies_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_snapshots": { + "name": "competitor_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "raw": { + "name": "raw", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_hash": { + "name": "raw_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "structured": { + "name": "structured", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "structured_hash": { + "name": "structured_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "competitor_snapshots_source_idx": { + "name": "competitor_snapshots_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_snapshots_competitor_id_competitors_id_fk": { + "name": "competitor_snapshots_competitor_id_competitors_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_snapshots_source_id_competitor_sources_id_fk": { + "name": "competitor_snapshots_source_id_competitor_sources_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "competitor_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_snapshots_company_id_companies_id_fk": { + "name": "competitor_snapshots_company_id_companies_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_sources": { + "name": "competitor_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "interval_hours": { + "name": "interval_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 168 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_state": { + "name": "health_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "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": { + "competitor_sources_company_idx": { + "name": "competitor_sources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "competitor_sources_competitor_idx": { + "name": "competitor_sources_competitor_idx", + "columns": [ + { + "expression": "competitor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_sources_competitor_id_competitors_id_fk": { + "name": "competitor_sources_competitor_id_competitors_id_fk", + "tableFrom": "competitor_sources", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_sources_company_id_companies_id_fk": { + "name": "competitor_sources_company_id_companies_id_fk", + "tableFrom": "competitor_sources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitors": { + "name": "competitors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_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 + }, + "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": { + "competitors_company_idx": { + "name": "competitors_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitors_company_id_companies_id_fk": { + "name": "competitors_company_id_companies_id_fk", + "tableFrom": "competitors", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.business_model": { + "name": "business_model", + "schema": "public", + "values": [ + "saas", + "marketplace", + "ecommerce", + "services", + "hardware", + "other" + ] + }, + "public.company_stage": { + "name": "company_stage", + "schema": "public", + "values": [ + "pre_seed", + "seed", + "series_a", + "series_b", + "series_c_plus", + "bootstrapped" + ] + }, + "public.data_region": { + "name": "data_region", + "schema": "public", + "values": [ + "us-east", + "eu-west", + "ap-south" + ] + }, + "public.invite_code_type": { + "name": "invite_code_type", + "schema": "public", + "values": [ + "single_use", + "multi_use" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "owner", + "admin", + "editor", + "viewer" + ] + }, + "public.integration_status": { + "name": "integration_status", + "schema": "public", + "values": [ + "active", + "disconnected", + "error" + ] + }, + "public.integration_type": { + "name": "integration_type", + "schema": "public", + "values": [ + "quickbooks", + "xero", + "freshbooks", + "plaid", + "mercury", + "gusto", + "stripe" + ] + }, + "public.mcp_auth_type": { + "name": "mcp_auth_type", + "schema": "public", + "values": [ + "oauth", + "pat", + "none" + ] + }, + "public.mcp_connection_status": { + "name": "mcp_connection_status", + "schema": "public", + "values": [ + "pending", + "connected", + "needs_auth", + "error", + "disabled" + ] + }, + "public.mcp_owner_scope": { + "name": "mcp_owner_scope", + "schema": "public", + "values": [ + "company", + "personal" + ] + }, + "public.mcp_tool_perm": { + "name": "mcp_tool_perm", + "schema": "public", + "values": [ + "read", + "write", + "delete" + ] + }, + "public.mcp_transport": { + "name": "mcp_transport", + "schema": "public", + "values": [ + "streamable_http", + "stdio" + ] + }, + "public.consent_purpose": { + "name": "consent_purpose", + "schema": "public", + "values": [ + "data_processing", + "ai_features", + "marketing", + "analytics" + ] + }, + "public.dashboard_mode": { + "name": "dashboard_mode", + "schema": "public", + "values": [ + "intelligence", + "dynamic", + "custom" + ] + }, + "public.notification_severity": { + "name": "notification_severity", + "schema": "public", + "values": [ + "info", + "success", + "warning", + "error" + ] + }, + "public.quick_action_mode": { + "name": "quick_action_mode", + "schema": "public", + "values": [ + "intelligence", + "dynamic", + "custom" + ] + }, + "public.scheduled_job_action_kind": { + "name": "scheduled_job_action_kind", + "schema": "public", + "values": [ + "write", + "notify" + ] + }, + "public.scheduled_job_notify_policy": { + "name": "scheduled_job_notify_policy", + "schema": "public", + "values": [ + "smart", + "failures", + "every", + "off" + ] + }, + "public.scheduled_job_run_status": { + "name": "scheduled_job_run_status", + "schema": "public", + "values": [ + "running", + "success", + "failed", + "missed" + ] + }, + "public.scheduled_job_run_trigger": { + "name": "scheduled_job_run_trigger", + "schema": "public", + "values": [ + "schedule", + "manual", + "dry_run" + ] + }, + "public.scheduled_job_status": { + "name": "scheduled_job_status", + "schema": "public", + "values": [ + "active", + "disabled", + "auto_disabled", + "error" + ] + }, + "public.ai_api_key_mode": { + "name": "ai_api_key_mode", + "schema": "public", + "values": [ + "managed", + "user_provided", + "none" + ] + }, + "public.ai_data_mode": { + "name": "ai_data_mode", + "schema": "public", + "values": [ + "full", + "show_cached", + "hide_all" + ] + }, + "public.ai_insight_cache_type": { + "name": "ai_insight_cache_type", + "schema": "public", + "values": [ + "dashboard", + "revenue", + "expense", + "scenario", + "funding", + "team", + "reports", + "general" + ] + }, + "public.ai_permission_mode": { + "name": "ai_permission_mode", + "schema": "public", + "values": [ + "ask", + "session", + "always" + ] + }, + "public.ai_provider_kind": { + "name": "ai_provider_kind", + "schema": "public", + "values": [ + "anthropic", + "openai", + "openrouter", + "ollama", + "google", + "mistral", + "groq", + "openai-compatible" + ] + }, + "public.ai_provider_model_source": { + "name": "ai_provider_model_source", + "schema": "public", + "values": [ + "fetched", + "manual", + "preset" + ] + }, + "public.ai_tool_audit_log_status": { + "name": "ai_tool_audit_log_status", + "schema": "public", + "values": [ + "success", + "error", + "validation_error", + "pending_apply" + ] + }, + "public.ai_tool_permission_decision": { + "name": "ai_tool_permission_decision", + "schema": "public", + "values": [ + "auto", + "granted_once", + "granted_session", + "denied" + ] + }, + "public.ai_turn_event_type": { + "name": "ai_turn_event_type", + "schema": "public", + "values": [ + "user_message", + "assistant_step", + "tool_result", + "scenario", + "gate", + "turn_done", + "turn_error" + ] + }, + "public.ai_write_mode": { + "name": "ai_write_mode", + "schema": "public", + "values": [ + "full", + "confirm", + "read_only" + ] + }, + "public.account_category": { + "name": "account_category", + "schema": "public", + "values": [ + "revenue", + "cogs", + "operating_expense", + "other_income", + "other_expense", + "asset", + "liability", + "equity" + ] + }, + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "income", + "expense", + "asset", + "liability", + "equity" + ] + }, + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "create", + "update", + "delete", + "import", + "rollback" + ] + }, + "public.audit_entity_type": { + "name": "audit_entity_type", + "schema": "public", + "values": [ + "transaction", + "financial_account", + "scenario", + "forecast_line", + "forecast_value", + "headcount_plan", + "revenue_stream", + "funding_round", + "import_batch", + "department", + "metric", + "salary_change", + "bonus", + "equity_grant", + "funding_round_investor", + "share_class", + "option_pool" + ] + }, + "public.bonus_type": { + "name": "bonus_type", + "schema": "public", + "values": [ + "signing", + "performance", + "retention", + "other" + ] + }, + "public.equity_grant_type": { + "name": "equity_grant_type", + "schema": "public", + "values": [ + "iso", + "nso", + "rsu" + ] + }, + "public.expense_frequency": { + "name": "expense_frequency", + "schema": "public", + "values": [ + "monthly", + "quarterly", + "annual" + ] + }, + "public.forecast_method": { + "name": "forecast_method", + "schema": "public", + "values": [ + "fixed", + "growth_rate", + "per_unit", + "percentage_of", + "custom_formula" + ] + }, + "public.funding_round_type": { + "name": "funding_round_type", + "schema": "public", + "values": [ + "pre_seed", + "seed", + "series_a", + "series_b", + "series_c_plus", + "debt", + "grant", + "safe", + "convertible" + ] + }, + "public.headcount_employee_type": { + "name": "headcount_employee_type", + "schema": "public", + "values": [ + "full_time", + "part_time", + "contractor" + ] + }, + "public.import_batch_status": { + "name": "import_batch_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "rolled_back", + "failed" + ] + }, + "public.metric_category": { + "name": "metric_category", + "schema": "public", + "values": [ + "financial", + "saas", + "growth", + "efficiency", + "custom" + ] + }, + "public.revenue_stream_type": { + "name": "revenue_stream_type", + "schema": "public", + "values": [ + "subscription", + "one_time", + "usage_based", + "services", + "marketplace", + "ecommerce", + "hardware" + ] + }, + "public.scenario_override_action": { + "name": "scenario_override_action", + "schema": "public", + "values": [ + "create", + "modify", + "delete" + ] + }, + "public.scenario_source": { + "name": "scenario_source", + "schema": "public", + "values": [ + "blank", + "ai", + "template", + "clone", + "backup" + ] + }, + "public.scenario_status": { + "name": "scenario_status", + "schema": "public", + "values": [ + "active", + "promoted", + "archived" + ] + }, + "public.share_class_type": { + "name": "share_class_type", + "schema": "public", + "values": [ + "common", + "preferred" + ] + }, + "public.transaction_source": { + "name": "transaction_source", + "schema": "public", + "values": [ + "manual", + "import", + "integration", + "forecast" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0016_snapshot.json b/packages/db/drizzle/meta/0016_snapshot.json new file mode 100644 index 00000000..c1fc2b26 --- /dev/null +++ b/packages/db/drizzle/meta/0016_snapshot.json @@ -0,0 +1,8828 @@ +{ + "id": "3bc3f20d-47d1-4707-8b06-2b9d56e11bce", + "prevId": "b5af6d6c-18e5-41c8-a653-6af8fa6c358a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "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 + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "accounts_user_idx": { + "name": "accounts_user_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": { + "accounts_provider_provider_account_id_pk": { + "name": "accounts_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_user_idx": { + "name": "sessions_user_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": {}, + "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": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "two_factor_backup_codes": { + "name": "two_factor_backup_codes", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_four": { + "name": "last_four", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "api_tokens_hash_idx": { + "name": "api_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_user_company_idx": { + "name": "api_tokens_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_tokens_company_idx": { + "name": "api_tokens_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_tokens_company_id_companies_id_fk": { + "name": "api_tokens_company_id_companies_id_fk", + "tableFrom": "api_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "company_stage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_seed'" + }, + "business_model": { + "name": "business_model", + "type": "business_model", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'saas'" + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "founded_date": { + "name": "founded_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "fiscal_year_end": { + "name": "fiscal_year_end", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 12 + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en-US'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'America/New_York'" + }, + "region": { + "name": "region", + "type": "data_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'us-east'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_provider": { + "name": "billing_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_subscription_id": { + "name": "billing_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_plan": { + "name": "billing_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'free'" + }, + "benefits_rates": { + "name": "benefits_rates", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "founders_ownership_percent": { + "name": "founders_ownership_percent", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": true, + "default": "'100.0000'" + }, + "mcp_server_enabled": { + "name": "mcp_server_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": {}, + "foreignKeys": { + "companies_owner_id_users_id_fk": { + "name": "companies_owner_id_users_id_fk", + "tableFrom": "companies", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_members": { + "name": "company_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "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": { + "company_member_unique": { + "name": "company_member_unique", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_member_user_idx": { + "name": "company_member_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_members_company_id_companies_id_fk": { + "name": "company_members_company_id_companies_id_fk", + "tableFrom": "company_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_members_user_id_users_id_fk": { + "name": "company_members_user_id_users_id_fk", + "tableFrom": "company_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_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": { + "departments_company_idx": { + "name": "departments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_company_id_companies_id_fk": { + "name": "departments_company_id_companies_id_fk", + "tableFrom": "departments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_code_redemptions": { + "name": "invite_code_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invite_code_id": { + "name": "invite_code_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_redemptions_code_idx": { + "name": "invite_redemptions_code_idx", + "columns": [ + { + "expression": "invite_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_redemptions_user_code_idx": { + "name": "invite_redemptions_user_code_idx", + "columns": [ + { + "expression": "invite_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_code_redemptions_invite_code_id_invite_codes_id_fk": { + "name": "invite_code_redemptions_invite_code_id_invite_codes_id_fk", + "tableFrom": "invite_code_redemptions", + "tableTo": "invite_codes", + "columnsFrom": [ + "invite_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invite_code_redemptions_user_id_users_id_fk": { + "name": "invite_code_redemptions_user_id_users_id_fk", + "tableFrom": "invite_code_redemptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "invite_code_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'single_use'" + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "current_redemptions": { + "name": "current_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "free_platform_days": { + "name": "free_platform_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "ai_credits_cents": { + "name": "ai_credits_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "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": { + "invite_codes_code_idx": { + "name": "invite_codes_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_codes_created_by_idx": { + "name": "invite_codes_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_codes_active_idx": { + "name": "invite_codes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_codes_created_by_users_id_fk": { + "name": "invite_codes_created_by_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_auth_codes": { + "name": "oauth_auth_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_auth_codes_hash_idx": { + "name": "oauth_auth_codes_hash_idx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_auth_codes_client_id_oauth_clients_id_fk": { + "name": "oauth_auth_codes_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_auth_codes_user_id_users_id_fk": { + "name": "oauth_auth_codes_user_id_users_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_auth_codes_company_id_companies_id_fk": { + "name": "oauth_auth_codes_company_id_companies_id_fk", + "tableFrom": "oauth_auth_codes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_hash": { + "name": "refresh_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "oauth_tokens_access_hash_idx": { + "name": "oauth_tokens_access_hash_idx", + "columns": [ + { + "expression": "access_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_refresh_hash_idx": { + "name": "oauth_tokens_refresh_hash_idx", + "columns": [ + { + "expression": "refresh_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_grant_idx": { + "name": "oauth_tokens_grant_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_user_company_idx": { + "name": "oauth_tokens_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_client_id_oauth_clients_id_fk": { + "name": "oauth_tokens_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_user_id_users_id_fk": { + "name": "oauth_tokens_user_id_users_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_company_id_companies_id_fk": { + "name": "oauth_tokens_company_id_companies_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_credentials": { + "name": "integration_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "integration_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "livemode": { + "name": "livemode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "integration_credentials_company_type_idx": { + "name": "integration_credentials_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_credentials_company_id_companies_id_fk": { + "name": "integration_credentials_company_id_companies_id_fk", + "tableFrom": "integration_credentials", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "integration_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "integrations_company_type_idx": { + "name": "integrations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrations_company_id_companies_id_fk": { + "name": "integrations_company_id_companies_id_fk", + "tableFrom": "integrations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_connections": { + "name": "mcp_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "mcp_owner_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "mcp_transport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "mcp_auth_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "status": { + "name": "status", + "type": "mcp_connection_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_connected_at": { + "name": "last_connected_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": { + "mcp_connections_company_idx": { + "name": "mcp_connections_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_owner_idx": { + "name": "mcp_connections_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_company_name_idx": { + "name": "mcp_connections_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_connections_company_slug_idx": { + "name": "mcp_connections_company_slug_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_connections_company_id_companies_id_fk": { + "name": "mcp_connections_company_id_companies_id_fk", + "tableFrom": "mcp_connections", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_connections_owner_user_id_users_id_fk": { + "name": "mcp_connections_owner_user_id_users_id_fk", + "tableFrom": "mcp_connections", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_connections_personal_owner_check": { + "name": "mcp_connections_personal_owner_check", + "value": "(owner_scope = 'personal') = (owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_credentials": { + "name": "mcp_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "mcp_auth_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_registration": { + "name": "client_registration", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_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": { + "mcp_credentials_connection_idx": { + "name": "mcp_credentials_connection_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_credentials_mcp_connection_id_mcp_connections_id_fk": { + "name": "mcp_credentials_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "mcp_credentials", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tool_prefs": { + "name": "mcp_tool_prefs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "perm_class_override": { + "name": "perm_class_override", + "type": "mcp_tool_perm", + "typeSchema": "public", + "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": { + "mcp_tool_prefs_connection_tool_idx": { + "name": "mcp_tool_prefs_connection_tool_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_tool_prefs_mcp_connection_id_mcp_connections_id_fk": { + "name": "mcp_tool_prefs_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "mcp_tool_prefs", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_preferences": { + "name": "dashboard_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "dashboard_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dynamic'" + }, + "hero_cards": { + "name": "hero_cards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "secondary_metrics": { + "name": "secondary_metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "card_mode_overrides": { + "name": "card_mode_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "card_scenario_overrides": { + "name": "card_scenario_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "custom_slug_overrides": { + "name": "custom_slug_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "slot_overrides": { + "name": "slot_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_metrics": { + "name": "custom_metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "closed_widgets": { + "name": "closed_widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "page_layouts": { + "name": "page_layouts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "dashboard_prefs_user_company_idx": { + "name": "dashboard_prefs_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_preferences_user_id_users_id_fk": { + "name": "dashboard_preferences_user_id_users_id_fk", + "tableFrom": "dashboard_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_preferences_company_id_companies_id_fk": { + "name": "dashboard_preferences_company_id_companies_id_fk", + "tableFrom": "dashboard_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.export_logs": { + "name": "export_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "export_type": { + "name": "export_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "export_logs_company_idx": { + "name": "export_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "export_logs_company_created_idx": { + "name": "export_logs_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "export_logs_user_idx": { + "name": "export_logs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "export_logs_company_id_companies_id_fk": { + "name": "export_logs_company_id_companies_id_fk", + "tableFrom": "export_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "export_logs_user_id_users_id_fk": { + "name": "export_logs_user_id_users_id_fk", + "tableFrom": "export_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "notification_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_unread_idx": { + "name": "notifications_unread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_company_id_companies_id_fk": { + "name": "notifications_company_id_companies_id_fk", + "tableFrom": "notifications", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.privacy_consents": { + "name": "privacy_consents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "consent_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "granted": { + "name": "granted", + "type": "boolean", + "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 + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "privacy_consents_user_idx": { + "name": "privacy_consents_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "privacy_consents_user_purpose_idx": { + "name": "privacy_consents_user_purpose_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purpose", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "privacy_consents_user_id_users_id_fk": { + "name": "privacy_consents_user_id_users_id_fk", + "tableFrom": "privacy_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_job_runs": { + "name": "scheduled_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scheduled_job_id": { + "name": "scheduled_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scheduled_job_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "scheduled_job_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_used": { + "name": "tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scheduled_job_runs_job_idx": { + "name": "scheduled_job_runs_job_idx", + "columns": [ + { + "expression": "scheduled_job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_job_runs_company_idx": { + "name": "scheduled_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_job_runs_scheduled_job_id_scheduled_jobs_id_fk": { + "name": "scheduled_job_runs_scheduled_job_id_scheduled_jobs_id_fk", + "tableFrom": "scheduled_job_runs", + "tableTo": "scheduled_jobs", + "columnsFrom": [ + "scheduled_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduled_job_runs_company_id_companies_id_fk": { + "name": "scheduled_job_runs_company_id_companies_id_fk", + "tableFrom": "scheduled_job_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_jobs": { + "name": "scheduled_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_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 + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_kind": { + "name": "action_kind", + "type": "scheduled_job_action_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bound_connection_ids": { + "name": "bound_connection_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "scheduled_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "notify_policy": { + "name": "notify_policy", + "type": "scheduled_job_notify_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'smart'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_run_cursor": { + "name": "last_run_cursor", + "type": "jsonb", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scheduled_jobs_company_idx": { + "name": "scheduled_jobs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduled_jobs_due_idx": { + "name": "scheduled_jobs_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_jobs_company_id_companies_id_fk": { + "name": "scheduled_jobs_company_id_companies_id_fk", + "tableFrom": "scheduled_jobs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduled_jobs_created_by_user_id_users_id_fk": { + "name": "scheduled_jobs_created_by_user_id_users_id_fk", + "tableFrom": "scheduled_jobs", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidebar_order": { + "name": "sidebar_order", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "quick_action_mode": { + "name": "quick_action_mode", + "type": "quick_action_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dynamic'" + }, + "quick_action_mode_overrides": { + "name": "quick_action_mode_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_quick_actions": { + "name": "custom_quick_actions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sidebar_collapsed": { + "name": "sidebar_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled_mcp_connections": { + "name": "disabled_mcp_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled_builtin_tools": { + "name": "disabled_builtin_tools", + "type": "jsonb", + "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": { + "user_preferences_user_company_idx": { + "name": "user_preferences_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_company_id_companies_id_fk": { + "name": "user_preferences_company_id_companies_id_fk", + "tableFrom": "user_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_digests": { + "name": "weekly_digests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "week_start": { + "name": "week_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "narrative": { + "name": "narrative", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deterministic_summary": { + "name": "deterministic_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_sent_at": { + "name": "email_sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "weekly_digests_company_idx": { + "name": "weekly_digests_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "weekly_digests_company_week_idx": { + "name": "weekly_digests_company_week_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weekly_digests_company_id_companies_id_fk": { + "name": "weekly_digests_company_id_companies_id_fk", + "tableFrom": "weekly_digests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_grants": { + "name": "session_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "session_disabled_tools": { + "name": "session_disabled_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "ai_conversations_company_idx": { + "name": "ai_conversations_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_conversations_company_user_idx": { + "name": "ai_conversations_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_company_id_companies_id_fk": { + "name": "ai_conversations_company_id_companies_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_feature_flags": { + "name": "ai_feature_flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "master_enabled": { + "name": "master_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "data_mode": { + "name": "data_mode", + "type": "ai_data_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "monthly_budget_cents": { + "name": "monthly_budget_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"onboarding\":true,\"chat\":true,\"insights\":true,\"uiPersonalization\":true,\"autoCategorization\":true,\"weeklyDigest\":true}'::jsonb" + }, + "write_mode": { + "name": "write_mode", + "type": "ai_write_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'confirm'" + }, + "companion_name": { + "name": "companion_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Companion'" + }, + "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": { + "ai_feature_flags_company_idx": { + "name": "ai_feature_flags_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_feature_flags_company_id_companies_id_fk": { + "name": "ai_feature_flags_company_id_companies_id_fk", + "tableFrom": "ai_feature_flags", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_insight_cache": { + "name": "ai_insight_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ai_insight_cache_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stale_at": { + "name": "stale_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stale_reason": { + "name": "stale_reason", + "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": { + "ai_insight_cache_company_idx": { + "name": "ai_insight_cache_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_insight_cache_company_key_idx": { + "name": "ai_insight_cache_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_insight_cache_company_id_companies_id_fk": { + "name": "ai_insight_cache_company_id_companies_id_fk", + "tableFrom": "ai_insight_cache", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_permission_defaults": { + "name": "ai_permission_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_mode": { + "name": "read_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "write_mode": { + "name": "write_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "delete_mode": { + "name": "delete_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "web_search_mode": { + "name": "web_search_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "browser_use_mode": { + "name": "browser_use_mode", + "type": "ai_permission_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "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": { + "ai_permission_defaults_user_company_idx": { + "name": "ai_permission_defaults_user_company_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_permission_defaults_user_id_users_id_fk": { + "name": "ai_permission_defaults_user_id_users_id_fk", + "tableFrom": "ai_permission_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_permission_defaults_company_id_companies_id_fk": { + "name": "ai_permission_defaults_company_id_companies_id_fk", + "tableFrom": "ai_permission_defaults", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_provider_models": { + "name": "ai_provider_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supports_tools": { + "name": "supports_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "supports_images": { + "name": "supports_images", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "ai_provider_model_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_provider_models_provider_idx": { + "name": "ai_provider_models_provider_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_provider_models_provider_model_idx": { + "name": "ai_provider_models_provider_model_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_provider_models_provider_id_ai_providers_id_fk": { + "name": "ai_provider_models_provider_id_ai_providers_id_fk", + "tableFrom": "ai_provider_models", + "tableTo": "ai_providers", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "ai_provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_mode": { + "name": "api_key_mode", + "type": "ai_api_key_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user_provided'" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "drop_params": { + "name": "drop_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_default": { + "name": "is_default", + "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": { + "ai_providers_company_idx": { + "name": "ai_providers_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_company_id_companies_id_fk": { + "name": "ai_providers_company_id_companies_id_fk", + "tableFrom": "ai_providers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_tool_audit_logs": { + "name": "ai_tool_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_connection_id": { + "name": "mcp_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_job_run_id": { + "name": "scheduled_job_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ai_tool_audit_log_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "permission_decision": { + "name": "permission_decision", + "type": "ai_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'chat'" + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_info": { + "name": "client_info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_tool_audit_company_idx": { + "name": "ai_tool_audit_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_user_idx": { + "name": "ai_tool_audit_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_created_idx": { + "name": "ai_tool_audit_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_tool_idx": { + "name": "ai_tool_audit_tool_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_conversation_idx": { + "name": "ai_tool_audit_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_mcp_connection_idx": { + "name": "ai_tool_audit_mcp_connection_idx", + "columns": [ + { + "expression": "mcp_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_tool_audit_scheduled_job_run_idx": { + "name": "ai_tool_audit_scheduled_job_run_idx", + "columns": [ + { + "expression": "scheduled_job_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_tool_audit_logs_company_id_companies_id_fk": { + "name": "ai_tool_audit_logs_company_id_companies_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_user_id_users_id_fk": { + "name": "ai_tool_audit_logs_user_id_users_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_conversation_id_ai_conversations_id_fk": { + "name": "ai_tool_audit_logs_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_mcp_connection_id_mcp_connections_id_fk": { + "name": "ai_tool_audit_logs_mcp_connection_id_mcp_connections_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "mcp_connections", + "columnsFrom": [ + "mcp_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_tool_audit_logs_scheduled_job_run_id_scheduled_job_runs_id_fk": { + "name": "ai_tool_audit_logs_scheduled_job_run_id_scheduled_job_runs_id_fk", + "tableFrom": "ai_tool_audit_logs", + "tableTo": "scheduled_job_runs", + "columnsFrom": [ + "scheduled_job_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_turn_events": { + "name": "ai_turn_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ai_turn_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_turn_events_conversation_seq_idx": { + "name": "ai_turn_events_conversation_seq_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_turn_events_open_gate_idx": { + "name": "ai_turn_events_open_gate_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"ai_turn_events\".\"type\" = 'gate' AND \"ai_turn_events\".\"resolved_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_turn_events_conversation_id_ai_conversations_id_fk": { + "name": "ai_turn_events_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_turn_events", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_logs": { + "name": "ai_usage_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "estimated_cost_micros": { + "name": "estimated_cost_micros", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_company_idx": { + "name": "ai_usage_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_feature_idx": { + "name": "ai_usage_feature_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_usage_created_idx": { + "name": "ai_usage_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_logs_company_id_companies_id_fk": { + "name": "ai_usage_logs_company_id_companies_id_fk", + "tableFrom": "ai_usage_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insight_invalidations": { + "name": "insight_invalidations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "insight_type": { + "name": "insight_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mutation_source": { + "name": "mutation_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_invalidated_at": { + "name": "first_invalidated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_mutation_at": { + "name": "last_mutation_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "insight_invalidations_company_type_idx": { + "name": "insight_invalidations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "insight_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insight_invalidations_pending_idx": { + "name": "insight_invalidations_pending_idx", + "columns": [ + { + "expression": "processed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insight_invalidations_company_id_companies_id_fk": { + "name": "insight_invalidations_company_id_companies_id_fk", + "tableFrom": "insight_invalidations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_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": { + "memory_company_idx": { + "name": "memory_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_company_domain_kind_idx": { + "name": "memory_company_domain_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_company_tier_idx": { + "name": "memory_company_tier_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_embedding_hnsw": { + "name": "memory_embedding_hnsw", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "where": "\"memory\".\"embedding\" IS NOT NULL", + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "memory_company_id_companies_id_fk": { + "name": "memory_company_id_companies_id_fk", + "tableFrom": "memory", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_user_id_users_id_fk": { + "name": "memory_user_id_users_id_fk", + "tableFrom": "memory", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bonuses": { + "name": "bonuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payout_month": { + "name": "payout_month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "bonus_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'performance'" + }, + "notes": { + "name": "notes", + "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": { + "bonuses_company_idx": { + "name": "bonuses_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bonuses_headcount_month_idx": { + "name": "bonuses_headcount_month_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payout_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bonuses_company_id_companies_id_fk": { + "name": "bonuses_company_id_companies_id_fk", + "tableFrom": "bonuses", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bonuses_headcount_id_headcount_plans_id_fk": { + "name": "bonuses_headcount_id_headcount_plans_id_fk", + "tableFrom": "bonuses", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.equity_grants": { + "name": "equity_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_date": { + "name": "grant_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "numeric(18, 4)", + "primaryKey": false, + "notNull": true + }, + "strike_price": { + "name": "strike_price", + "type": "numeric(18, 4)", + "primaryKey": false, + "notNull": false + }, + "grant_type": { + "name": "grant_type", + "type": "equity_grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'iso'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "equity_grants_company_idx": { + "name": "equity_grants_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "equity_grants_headcount_idx": { + "name": "equity_grants_headcount_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "equity_grants_company_id_companies_id_fk": { + "name": "equity_grants_company_id_companies_id_fk", + "tableFrom": "equity_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "equity_grants_headcount_id_headcount_plans_id_fk": { + "name": "equity_grants_headcount_id_headcount_plans_id_fk", + "tableFrom": "equity_grants", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_accounts": { + "name": "financial_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "account_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "covers_headcount": { + "name": "covers_headcount", + "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": { + "financial_accounts_company_idx": { + "name": "financial_accounts_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_accounts_parent_idx": { + "name": "financial_accounts_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "financial_accounts_company_id_companies_id_fk": { + "name": "financial_accounts_company_id_companies_id_fk", + "tableFrom": "financial_accounts", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_audit_logs": { + "name": "financial_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "audit_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "financial_audit_company_idx": { + "name": "financial_audit_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_entity_idx": { + "name": "financial_audit_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_user_idx": { + "name": "financial_audit_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "financial_audit_created_idx": { + "name": "financial_audit_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "financial_audit_logs_company_id_companies_id_fk": { + "name": "financial_audit_logs_company_id_companies_id_fk", + "tableFrom": "financial_audit_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "financial_audit_logs_user_id_users_id_fk": { + "name": "financial_audit_logs_user_id_users_id_fk", + "tableFrom": "financial_audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forecast_lines": { + "name": "forecast_lines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "forecast_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "expense_frequency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + }, + "is_one_time": { + "name": "is_one_time", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recurring": { + "name": "is_recurring", + "type": "boolean", + "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": { + "forecast_lines_company_idx": { + "name": "forecast_lines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_account_idx": { + "name": "forecast_lines_company_account_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_department_idx": { + "name": "forecast_lines_company_department_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_vendor_idx": { + "name": "forecast_lines_vendor_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vendor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_lines_company_name_idx": { + "name": "forecast_lines_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"forecast_lines\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forecast_lines_company_id_companies_id_fk": { + "name": "forecast_lines_company_id_companies_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forecast_lines_account_id_financial_accounts_id_fk": { + "name": "forecast_lines_account_id_financial_accounts_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forecast_lines_department_id_departments_id_fk": { + "name": "forecast_lines_department_id_departments_id_fk", + "tableFrom": "forecast_lines", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forecast_values": { + "name": "forecast_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "forecast_line_id": { + "name": "forecast_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "is_override": { + "name": "is_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "forecast_values_line_idx": { + "name": "forecast_values_line_idx", + "columns": [ + { + "expression": "forecast_line_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_values_month_idx": { + "name": "forecast_values_month_idx", + "columns": [ + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forecast_values_line_month_idx": { + "name": "forecast_values_line_month_idx", + "columns": [ + { + "expression": "forecast_line_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forecast_values_forecast_line_id_forecast_lines_id_fk": { + "name": "forecast_values_forecast_line_id_forecast_lines_id_fk", + "tableFrom": "forecast_values", + "tableTo": "forecast_lines", + "columnsFrom": [ + "forecast_line_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_round_investors": { + "name": "funding_round_investors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "funding_round_id": { + "name": "funding_round_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_invested": { + "name": "amount_invested", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "funding_round_investors_round_idx": { + "name": "funding_round_investors_round_idx", + "columns": [ + { + "expression": "funding_round_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "funding_round_investors_funding_round_id_funding_rounds_id_fk": { + "name": "funding_round_investors_funding_round_id_funding_rounds_id_fk", + "tableFrom": "funding_round_investors", + "tableTo": "funding_rounds", + "columnsFrom": [ + "funding_round_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_rounds": { + "name": "funding_rounds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "funding_round_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "pre_money_valuation": { + "name": "pre_money_valuation", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "dilution_percent": { + "name": "dilution_percent", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": false + }, + "is_projected": { + "name": "is_projected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "close_date": { + "name": "close_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "funding_rounds_company_idx": { + "name": "funding_rounds_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "funding_rounds_company_id_companies_id_fk": { + "name": "funding_rounds_company_id_companies_id_fk", + "tableFrom": "funding_rounds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.headcount_plans": { + "name": "headcount_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "employee_type": { + "name": "employee_type", + "type": "headcount_employee_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full_time'" + }, + "count": { + "name": "count", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'1.00'" + }, + "salary": { + "name": "salary", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "hourly_rate": { + "name": "hourly_rate", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "hours_per_week": { + "name": "hours_per_week", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "benefits_rate": { + "name": "benefits_rate", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0.20'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "headcount_plans_company_idx": { + "name": "headcount_plans_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "headcount_plans_department_idx": { + "name": "headcount_plans_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "headcount_plans_company_id_companies_id_fk": { + "name": "headcount_plans_company_id_companies_id_fk", + "tableFrom": "headcount_plans", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "headcount_plans_department_id_departments_id_fk": { + "name": "headcount_plans_department_id_departments_id_fk", + "tableFrom": "headcount_plans", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.import_batches": { + "name": "import_batches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "import_batch_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "total_rows": { + "name": "total_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "imported_count": { + "name": "imported_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "column_mapping": { + "name": "column_mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_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": { + "import_batches_company_idx": { + "name": "import_batches_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "import_batches_account_idx": { + "name": "import_batches_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "import_batches_company_id_companies_id_fk": { + "name": "import_batches_company_id_companies_id_fk", + "tableFrom": "import_batches", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "import_batches_account_id_financial_accounts_id_fk": { + "name": "import_batches_account_id_financial_accounts_id_fk", + "tableFrom": "import_batches", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.merchant_category_mappings": { + "name": "merchant_category_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merchant_pattern": { + "name": "merchant_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "account_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user_override'" + }, + "override_count": { + "name": "override_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": { + "merchant_mappings_company_idx": { + "name": "merchant_mappings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_pattern_idx": { + "name": "merchant_mappings_pattern_idx", + "columns": [ + { + "expression": "merchant_pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_account_idx": { + "name": "merchant_mappings_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "merchant_mappings_company_pattern_idx": { + "name": "merchant_mappings_company_pattern_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "merchant_pattern", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "merchant_category_mappings_company_id_companies_id_fk": { + "name": "merchant_category_mappings_company_id_companies_id_fk", + "tableFrom": "merchant_category_mappings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "merchant_category_mappings_account_id_financial_accounts_id_fk": { + "name": "merchant_category_mappings_account_id_financial_accounts_id_fk", + "tableFrom": "merchant_category_mappings", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metrics": { + "name": "metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formula": { + "name": "formula", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "category": { + "name": "category", + "type": "metric_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'financial'" + }, + "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": { + "metrics_company_slug_idx": { + "name": "metrics_company_slug_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metrics_company_id_companies_id_fk": { + "name": "metrics_company_id_companies_id_fk", + "tableFrom": "metrics", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.option_pools": { + "name": "option_pools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_reserved": { + "name": "total_reserved", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true + }, + "refresh_date": { + "name": "refresh_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "option_pools_company_idx": { + "name": "option_pools_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "option_pools_company_id_companies_id_fk": { + "name": "option_pools_company_id_companies_id_fk", + "tableFrom": "option_pools", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revenue_streams": { + "name": "revenue_streams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "revenue_stream_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'subscription'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "end_date": { + "name": "end_date", + "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": { + "revenue_streams_company_idx": { + "name": "revenue_streams_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "revenue_streams_active_idx": { + "name": "revenue_streams_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revenue_streams_company_id_companies_id_fk": { + "name": "revenue_streams_company_id_companies_id_fk", + "tableFrom": "revenue_streams", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.salary_changes": { + "name": "salary_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headcount_id": { + "name": "headcount_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_date": { + "name": "effective_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "new_salary": { + "name": "new_salary", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "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": { + "salary_changes_company_idx": { + "name": "salary_changes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "salary_changes_headcount_date_idx": { + "name": "salary_changes_headcount_date_idx", + "columns": [ + { + "expression": "headcount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "salary_changes_company_id_companies_id_fk": { + "name": "salary_changes_company_id_companies_id_fk", + "tableFrom": "salary_changes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "salary_changes_headcount_id_headcount_plans_id_fk": { + "name": "salary_changes_headcount_id_headcount_plans_id_fk", + "tableFrom": "salary_changes", + "tableTo": "headcount_plans", + "columnsFrom": [ + "headcount_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario_overrides": { + "name": "scenario_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "scenario_override_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "original_data": { + "name": "original_data", + "type": "jsonb", + "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": { + "scenario_overrides_unique": { + "name": "scenario_overrides_unique", + "columns": [ + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scenario_overrides_scenario_type": { + "name": "scenario_overrides_scenario_type", + "columns": [ + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_overrides_scenario_id_scenarios_id_fk": { + "name": "scenario_overrides_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_overrides", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenarios": { + "name": "scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "scenario_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'blank'" + }, + "status": { + "name": "status", + "type": "scenario_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_scenario_id": { + "name": "source_scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_conversation_id": { + "name": "ai_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auto_delete_at": { + "name": "auto_delete_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "scenarios_company_idx": { + "name": "scenarios_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenarios_company_id_companies_id_fk": { + "name": "scenarios_company_id_companies_id_fk", + "tableFrom": "scenarios", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_classes": { + "name": "share_classes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "class_type": { + "name": "class_type", + "type": "share_class_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "total_authorized": { + "name": "total_authorized", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true + }, + "total_issued": { + "name": "total_issued", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "par_value": { + "name": "par_value", + "type": "numeric(18, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0.000001'" + }, + "liquidation_preference": { + "name": "liquidation_preference", + "type": "numeric(7, 4)", + "primaryKey": false, + "notNull": true, + "default": "'1.0000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "share_classes_company_idx": { + "name": "share_classes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_classes_company_id_companies_id_fk": { + "name": "share_classes_company_id_companies_id_fk", + "tableFrom": "share_classes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "transaction_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_batch_id": { + "name": "import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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": { + "transactions_company_date_idx": { + "name": "transactions_company_date_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_account_idx": { + "name": "transactions_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_external_id_idx": { + "name": "transactions_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_batch_idx": { + "name": "transactions_batch_idx", + "columns": [ + { + "expression": "import_batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_company_id_companies_id_fk": { + "name": "transactions_company_id_companies_id_fk", + "tableFrom": "transactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transactions_account_id_financial_accounts_id_fk": { + "name": "transactions_account_id_financial_accounts_id_fk", + "tableFrom": "transactions", + "tableTo": "financial_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_changes": { + "name": "competitor_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_at": { + "name": "detected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before": { + "name": "before", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after": { + "name": "after", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "competitor_changes_company_idx": { + "name": "competitor_changes_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "detected_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "competitor_changes_competitor_idx": { + "name": "competitor_changes_competitor_idx", + "columns": [ + { + "expression": "competitor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_changes_competitor_id_competitors_id_fk": { + "name": "competitor_changes_competitor_id_competitors_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_source_id_competitor_sources_id_fk": { + "name": "competitor_changes_source_id_competitor_sources_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitor_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_snapshot_id_competitor_snapshots_id_fk": { + "name": "competitor_changes_snapshot_id_competitor_snapshots_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "competitor_snapshots", + "columnsFrom": [ + "snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_changes_company_id_companies_id_fk": { + "name": "competitor_changes_company_id_companies_id_fk", + "tableFrom": "competitor_changes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_snapshots": { + "name": "competitor_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "raw": { + "name": "raw", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_hash": { + "name": "raw_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "structured": { + "name": "structured", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "structured_hash": { + "name": "structured_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized": { + "name": "normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_hash": { + "name": "normalized_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "competitor_snapshots_source_idx": { + "name": "competitor_snapshots_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_snapshots_competitor_id_competitors_id_fk": { + "name": "competitor_snapshots_competitor_id_competitors_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_snapshots_source_id_competitor_sources_id_fk": { + "name": "competitor_snapshots_source_id_competitor_sources_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "competitor_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_snapshots_company_id_companies_id_fk": { + "name": "competitor_snapshots_company_id_companies_id_fk", + "tableFrom": "competitor_snapshots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitor_sources": { + "name": "competitor_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_id": { + "name": "competitor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "interval_hours": { + "name": "interval_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 168 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_state": { + "name": "health_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "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": { + "competitor_sources_company_idx": { + "name": "competitor_sources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "competitor_sources_competitor_idx": { + "name": "competitor_sources_competitor_idx", + "columns": [ + { + "expression": "competitor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitor_sources_competitor_id_competitors_id_fk": { + "name": "competitor_sources_competitor_id_competitors_id_fk", + "tableFrom": "competitor_sources", + "tableTo": "competitors", + "columnsFrom": [ + "competitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "competitor_sources_company_id_companies_id_fk": { + "name": "competitor_sources_company_id_companies_id_fk", + "tableFrom": "competitor_sources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.competitors": { + "name": "competitors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_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 + }, + "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": { + "competitors_company_idx": { + "name": "competitors_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "competitors_company_id_companies_id_fk": { + "name": "competitors_company_id_companies_id_fk", + "tableFrom": "competitors", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.business_model": { + "name": "business_model", + "schema": "public", + "values": [ + "saas", + "marketplace", + "ecommerce", + "services", + "hardware", + "other" + ] + }, + "public.company_stage": { + "name": "company_stage", + "schema": "public", + "values": [ + "pre_seed", + "seed", + "series_a", + "series_b", + "series_c_plus", + "bootstrapped" + ] + }, + "public.data_region": { + "name": "data_region", + "schema": "public", + "values": [ + "us-east", + "eu-west", + "ap-south" + ] + }, + "public.invite_code_type": { + "name": "invite_code_type", + "schema": "public", + "values": [ + "single_use", + "multi_use" + ] + }, + "public.member_role": { + "name": "member_role", + "schema": "public", + "values": [ + "owner", + "admin", + "editor", + "viewer" + ] + }, + "public.integration_status": { + "name": "integration_status", + "schema": "public", + "values": [ + "active", + "disconnected", + "error" + ] + }, + "public.integration_type": { + "name": "integration_type", + "schema": "public", + "values": [ + "quickbooks", + "xero", + "freshbooks", + "plaid", + "mercury", + "gusto", + "stripe" + ] + }, + "public.mcp_auth_type": { + "name": "mcp_auth_type", + "schema": "public", + "values": [ + "oauth", + "pat", + "none" + ] + }, + "public.mcp_connection_status": { + "name": "mcp_connection_status", + "schema": "public", + "values": [ + "pending", + "connected", + "needs_auth", + "error", + "disabled" + ] + }, + "public.mcp_owner_scope": { + "name": "mcp_owner_scope", + "schema": "public", + "values": [ + "company", + "personal" + ] + }, + "public.mcp_tool_perm": { + "name": "mcp_tool_perm", + "schema": "public", + "values": [ + "read", + "write", + "delete" + ] + }, + "public.mcp_transport": { + "name": "mcp_transport", + "schema": "public", + "values": [ + "streamable_http", + "stdio" + ] + }, + "public.consent_purpose": { + "name": "consent_purpose", + "schema": "public", + "values": [ + "data_processing", + "ai_features", + "marketing", + "analytics" + ] + }, + "public.dashboard_mode": { + "name": "dashboard_mode", + "schema": "public", + "values": [ + "intelligence", + "dynamic", + "custom" + ] + }, + "public.notification_severity": { + "name": "notification_severity", + "schema": "public", + "values": [ + "info", + "success", + "warning", + "error" + ] + }, + "public.quick_action_mode": { + "name": "quick_action_mode", + "schema": "public", + "values": [ + "intelligence", + "dynamic", + "custom" + ] + }, + "public.scheduled_job_action_kind": { + "name": "scheduled_job_action_kind", + "schema": "public", + "values": [ + "write", + "notify" + ] + }, + "public.scheduled_job_notify_policy": { + "name": "scheduled_job_notify_policy", + "schema": "public", + "values": [ + "smart", + "failures", + "every", + "off" + ] + }, + "public.scheduled_job_run_status": { + "name": "scheduled_job_run_status", + "schema": "public", + "values": [ + "running", + "success", + "failed", + "missed" + ] + }, + "public.scheduled_job_run_trigger": { + "name": "scheduled_job_run_trigger", + "schema": "public", + "values": [ + "schedule", + "manual", + "dry_run" + ] + }, + "public.scheduled_job_status": { + "name": "scheduled_job_status", + "schema": "public", + "values": [ + "active", + "disabled", + "auto_disabled", + "error" + ] + }, + "public.ai_api_key_mode": { + "name": "ai_api_key_mode", + "schema": "public", + "values": [ + "managed", + "user_provided", + "none" + ] + }, + "public.ai_data_mode": { + "name": "ai_data_mode", + "schema": "public", + "values": [ + "full", + "show_cached", + "hide_all" + ] + }, + "public.ai_insight_cache_type": { + "name": "ai_insight_cache_type", + "schema": "public", + "values": [ + "dashboard", + "revenue", + "expense", + "scenario", + "funding", + "team", + "reports", + "general" + ] + }, + "public.ai_permission_mode": { + "name": "ai_permission_mode", + "schema": "public", + "values": [ + "ask", + "session", + "always" + ] + }, + "public.ai_provider_kind": { + "name": "ai_provider_kind", + "schema": "public", + "values": [ + "anthropic", + "openai", + "openrouter", + "ollama", + "google", + "mistral", + "groq", + "openai-compatible" + ] + }, + "public.ai_provider_model_source": { + "name": "ai_provider_model_source", + "schema": "public", + "values": [ + "fetched", + "manual", + "preset" + ] + }, + "public.ai_tool_audit_log_status": { + "name": "ai_tool_audit_log_status", + "schema": "public", + "values": [ + "success", + "error", + "validation_error", + "pending_apply" + ] + }, + "public.ai_tool_permission_decision": { + "name": "ai_tool_permission_decision", + "schema": "public", + "values": [ + "auto", + "granted_once", + "granted_session", + "denied" + ] + }, + "public.ai_turn_event_type": { + "name": "ai_turn_event_type", + "schema": "public", + "values": [ + "user_message", + "assistant_step", + "tool_result", + "scenario", + "gate", + "turn_done", + "turn_error" + ] + }, + "public.ai_write_mode": { + "name": "ai_write_mode", + "schema": "public", + "values": [ + "full", + "confirm", + "read_only" + ] + }, + "public.account_category": { + "name": "account_category", + "schema": "public", + "values": [ + "revenue", + "cogs", + "operating_expense", + "other_income", + "other_expense", + "asset", + "liability", + "equity" + ] + }, + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "income", + "expense", + "asset", + "liability", + "equity" + ] + }, + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "create", + "update", + "delete", + "import", + "rollback" + ] + }, + "public.audit_entity_type": { + "name": "audit_entity_type", + "schema": "public", + "values": [ + "transaction", + "financial_account", + "scenario", + "forecast_line", + "forecast_value", + "headcount_plan", + "revenue_stream", + "funding_round", + "import_batch", + "department", + "metric", + "salary_change", + "bonus", + "equity_grant", + "funding_round_investor", + "share_class", + "option_pool" + ] + }, + "public.bonus_type": { + "name": "bonus_type", + "schema": "public", + "values": [ + "signing", + "performance", + "retention", + "other" + ] + }, + "public.equity_grant_type": { + "name": "equity_grant_type", + "schema": "public", + "values": [ + "iso", + "nso", + "rsu" + ] + }, + "public.expense_frequency": { + "name": "expense_frequency", + "schema": "public", + "values": [ + "monthly", + "quarterly", + "annual" + ] + }, + "public.forecast_method": { + "name": "forecast_method", + "schema": "public", + "values": [ + "fixed", + "growth_rate", + "per_unit", + "percentage_of", + "custom_formula" + ] + }, + "public.funding_round_type": { + "name": "funding_round_type", + "schema": "public", + "values": [ + "pre_seed", + "seed", + "series_a", + "series_b", + "series_c_plus", + "debt", + "grant", + "safe", + "convertible" + ] + }, + "public.headcount_employee_type": { + "name": "headcount_employee_type", + "schema": "public", + "values": [ + "full_time", + "part_time", + "contractor" + ] + }, + "public.import_batch_status": { + "name": "import_batch_status", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "rolled_back", + "failed" + ] + }, + "public.metric_category": { + "name": "metric_category", + "schema": "public", + "values": [ + "financial", + "saas", + "growth", + "efficiency", + "custom" + ] + }, + "public.revenue_stream_type": { + "name": "revenue_stream_type", + "schema": "public", + "values": [ + "subscription", + "one_time", + "usage_based", + "services", + "marketplace", + "ecommerce", + "hardware" + ] + }, + "public.scenario_override_action": { + "name": "scenario_override_action", + "schema": "public", + "values": [ + "create", + "modify", + "delete" + ] + }, + "public.scenario_source": { + "name": "scenario_source", + "schema": "public", + "values": [ + "blank", + "ai", + "template", + "clone", + "backup" + ] + }, + "public.scenario_status": { + "name": "scenario_status", + "schema": "public", + "values": [ + "active", + "promoted", + "archived" + ] + }, + "public.share_class_type": { + "name": "share_class_type", + "schema": "public", + "values": [ + "common", + "preferred" + ] + }, + "public.transaction_source": { + "name": "transaction_source", + "schema": "public", + "values": [ + "manual", + "import", + "integration", + "forecast" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 4f88c920..26cdfefb 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -106,6 +106,20 @@ "when": 1782459274352, "tag": "0014_curvy_shinobi_shaw", "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1782709952361, + "tag": "0015_shocking_lord_tyger", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1783150210962, + "tag": "0016_sad_moondragon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/__tests__/competitor.test.ts b/packages/db/src/__tests__/competitor.test.ts new file mode 100644 index 00000000..8d5d614c --- /dev/null +++ b/packages/db/src/__tests__/competitor.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { getTestDb } from "./setup"; + +vi.mock("../index", () => ({ + get db() { + return getTestDb(); + }, +})); + +import { createUser, createCompany } from "./factories"; +import { + createCompetitor, + listCompetitors, + getCompetitor, + updateCompetitor, + deleteCompetitor, + createSource, + getDueSources, + getLatestSnapshot, + insertSnapshot, + insertChanges, + listChanges, +} from "../queries/competitor"; + +let companyId: string; + +beforeEach(async () => { + const owner = await createUser(); + const company = await createCompany(owner.id); + companyId = company.id; +}); + +describe("competitor queries", () => { + it("creates and lists competitors scoped by company", async () => { + await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const list = await listCompetitors(companyId); + expect(list).toHaveLength(1); + expect(list.some((c) => c.name === "Acme")).toBe(true); + }); + + it("getCompetitor returns undefined for wrong companyId", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const result = await getCompetitor(c.id, "wrong-company-id"); + expect(result).toBeUndefined(); + }); + + it("updateCompetitor patches name and is company-scoped", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const updated = await updateCompetitor(c.id, companyId, { name: "Acme Corp" }); + expect(updated?.name).toBe("Acme Corp"); + // wrong company returns undefined + const miss = await updateCompetitor(c.id, "other", { name: "X" }); + expect(miss).toBeUndefined(); + }); + + it("getDueSources returns sources never run or past their interval", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + await createSource({ + companyId, + competitorId: c.id, + type: "pricing", + url: "https://acme.com/pricing", + }); + const due = await getDueSources(new Date()); + // lastRunAt is null → due immediately + expect(due).toHaveLength(1); + }); + + it("insertSnapshot + getLatestSnapshot returns the most recent by source", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const s = await createSource({ companyId, competitorId: c.id, type: "pricing", url: "u" }); + await insertSnapshot({ + companyId, + competitorId: c.id, + sourceId: s.id, + raw: "a", + rawHash: "h1", + structured: { v: 1 }, + structuredHash: "sh1", + normalized: "n/a", + normalizedHash: "n/a", + }); + const latest = await getLatestSnapshot(s.id); + expect(latest?.structuredHash).toBe("sh1"); + }); + + it("insertChanges + listChanges by company ordered by detectedAt desc", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const s = await createSource({ companyId, competitorId: c.id, type: "pricing", url: "u" }); + const snap = await insertSnapshot({ + companyId, + competitorId: c.id, + sourceId: s.id, + raw: "a", + rawHash: "h", + structured: {}, + structuredHash: "sh", + normalized: "n/a", + normalizedHash: "n/a", + }); + await insertChanges([ + { + companyId, + competitorId: c.id, + sourceId: s.id, + snapshotId: snap.id, + changeType: "price_increase", + summary: "x", + severity: "warning", + }, + ]); + const changes = await listChanges(companyId); + expect(changes).toHaveLength(1); + expect(changes.some((ch) => ch.changeType === "price_increase")).toBe(true); + }); + + it("deleteCompetitor cascades and is company-scoped", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + await deleteCompetitor(c.id, companyId); + expect(await getCompetitor(c.id, companyId)).toBeUndefined(); + }); + + it("round-trips normalized text + hash on a snapshot", async () => { + const c = await createCompetitor({ companyId, name: "Acme", url: "https://acme.com" }); + const s = await createSource({ companyId, competitorId: c.id, type: "pricing", url: "u" }); + const snap = await insertSnapshot({ + companyId, + competitorId: c.id, + sourceId: s.id, + raw: "

Pro $29

", + rawHash: "rawhash", + structured: { plans: [] }, + structuredHash: "structhash", + normalized: "Pro $29", + normalizedHash: "normhash", + }); + expect(snap.normalized).toBe("Pro $29"); + expect(snap.normalizedHash).toBe("normhash"); + + const latest = await getLatestSnapshot(s.id); + expect(latest?.normalized).toBe("Pro $29"); + expect(latest?.normalizedHash).toBe("normhash"); + }); +}); diff --git a/packages/db/src/queries/company.ts b/packages/db/src/queries/company.ts index ca6d1ade..03463e1b 100644 --- a/packages/db/src/queries/company.ts +++ b/packages/db/src/queries/company.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { db } from "../index"; import { companies, companyMembers, users } from "../schema"; @@ -48,6 +48,24 @@ export async function getCompanyById(companyId: string) { return row ?? null; } +/** + * Return the userIds of all owner and admin members for a company. + * Used to fan-out notifications (e.g. competitor change alerts) to the right + * recipients without exposing editor/viewer members to ops events. + */ +export async function getCompanyNotifyUserIds(companyId: string): Promise { + const rows = await db + .select({ userId: companyMembers.userId }) + .from(companyMembers) + .where( + and( + eq(companyMembers.companyId, companyId), + inArray(companyMembers.role, ["owner", "admin"]), + ), + ); + return rows.map((r) => r.userId); +} + /** All memberships of a user with company display fields — used by the OAuth * consent company picker (expose spec §5.2: multi-company users pick the * tenant a grant is bound to). */ diff --git a/packages/db/src/queries/competitor.ts b/packages/db/src/queries/competitor.ts new file mode 100644 index 00000000..cf604e47 --- /dev/null +++ b/packages/db/src/queries/competitor.ts @@ -0,0 +1,203 @@ +import { and, desc, eq } from "drizzle-orm"; +import { db } from "../index"; +import { + competitors, + competitorSources, + competitorSnapshots, + competitorChanges, + type Competitor, + type CompetitorSource, + type CompetitorSnapshot, + type CompetitorChange, +} from "../schema/competitor"; + +// ── Competitors ─────────────────────────────────────────────────────────────── + +export async function createCompetitor(input: { + companyId: string; + name: string; + url: string; + status?: string; +}): Promise { + const [row] = await db.insert(competitors).values(input).returning(); + return row!; +} + +export async function listCompetitors(companyId: string): Promise { + return db + .select() + .from(competitors) + .where(eq(competitors.companyId, companyId)) + .orderBy(desc(competitors.createdAt)); +} + +export async function getCompetitor( + id: string, + companyId: string, +): Promise { + const [row] = await db + .select() + .from(competitors) + .where(and(eq(competitors.id, id), eq(competitors.companyId, companyId))); + return row; +} + +export async function updateCompetitor( + id: string, + companyId: string, + patch: Partial>, +): Promise { + const [row] = await db + .update(competitors) + .set({ ...patch, updatedAt: new Date() }) + .where(and(eq(competitors.id, id), eq(competitors.companyId, companyId))) + .returning(); + return row; +} + +export async function deleteCompetitor(id: string, companyId: string): Promise { + await db + .delete(competitors) + .where(and(eq(competitors.id, id), eq(competitors.companyId, companyId))); +} + +// ── Competitor Sources ──────────────────────────────────────────────────────── + +export async function createSource(input: { + companyId: string; + competitorId: string; + type: string; + url: string; + config?: unknown; + intervalHours?: number; +}): Promise { + const [row] = await db + .insert(competitorSources) + .values(input as typeof competitorSources.$inferInsert) + .returning(); + return row!; +} + +export async function listSources( + competitorId: string, + companyId: string, +): Promise { + return db + .select() + .from(competitorSources) + .where( + and( + eq(competitorSources.competitorId, competitorId), + eq(competitorSources.companyId, companyId), + ), + ); +} + +export async function updateSource( + id: string, + companyId: string, + patch: Partial, +): Promise { + const [row] = await db + .update(competitorSources) + .set({ ...patch, updatedAt: new Date() }) + .where(and(eq(competitorSources.id, id), eq(competitorSources.companyId, companyId))) + .returning(); + return row; +} + +export async function deleteSource(id: string, companyId: string): Promise { + await db + .delete(competitorSources) + .where(and(eq(competitorSources.id, id), eq(competitorSources.companyId, companyId))); +} + +/** + * Sources that are enabled and due to run: + * - never run (lastRunAt IS NULL), OR + * - lastRunAt + intervalHours hours <= now + * + * Uses a JS-side filter for PGlite compatibility (interval arithmetic in WHERE + * can behave inconsistently across PGlite versions). Correct for the expected + * scale of competitor sources (typically tens of rows, never thousands). + */ +export async function getDueSources(now: Date, limit = 200): Promise { + const rows = await db + .select() + .from(competitorSources) + .where(eq(competitorSources.enabled, true)); + + return rows + .filter( + (r) => + r.lastRunAt == null || + r.lastRunAt.getTime() + r.intervalHours * 3_600_000 <= now.getTime(), + ) + .slice(0, limit); +} + +// ── Competitor Snapshots ────────────────────────────────────────────────────── + +export async function getLatestSnapshot( + sourceId: string, +): Promise { + const [row] = await db + .select() + .from(competitorSnapshots) + .where(eq(competitorSnapshots.sourceId, sourceId)) + .orderBy(desc(competitorSnapshots.capturedAt)) + .limit(1); + return row; +} + +export async function insertSnapshot(input: { + companyId: string; + competitorId: string; + sourceId: string; + raw: string; + rawHash: string; + structured: unknown; + structuredHash: string; + normalized: string; + normalizedHash: string; +}): Promise { + const [row] = await db + .insert(competitorSnapshots) + .values(input as typeof competitorSnapshots.$inferInsert) + .returning(); + return row!; +} + +// ── Competitor Changes ──────────────────────────────────────────────────────── + +export async function insertChanges( + rows: Array>, +): Promise { + if (rows.length === 0) return; + await db + .insert(competitorChanges) + .values(rows as typeof competitorChanges.$inferInsert[]); +} + +export async function listChanges( + companyId: string, + opts?: { competitorId?: string; limit?: number }, +): Promise { + const conds: ReturnType[] = [eq(competitorChanges.companyId, companyId)]; + if (opts?.competitorId) { + conds.push(eq(competitorChanges.competitorId, opts.competitorId)); + } + return db + .select() + .from(competitorChanges) + .where(conds.length === 1 ? conds[0] : and(...conds)) + .orderBy(desc(competitorChanges.detectedAt)) + .limit(opts?.limit ?? 100); +} + +export async function ackChange(id: string, companyId: string, at: Date): Promise { + await db + .update(competitorChanges) + .set({ acknowledgedAt: at }) + .where(and(eq(competitorChanges.id, id), eq(competitorChanges.companyId, companyId))); +} diff --git a/packages/db/src/queries/index.ts b/packages/db/src/queries/index.ts index dc905dbf..e1734b72 100644 --- a/packages/db/src/queries/index.ts +++ b/packages/db/src/queries/index.ts @@ -3,6 +3,7 @@ export { getUserWithCompany, getCompanyById, listCompaniesForUser, + getCompanyNotifyUserIds, } from "./company"; export { @@ -181,6 +182,24 @@ export { type OauthGrantSummary, } from "./oauth"; +export { + createCompetitor, + listCompetitors, + getCompetitor, + updateCompetitor, + deleteCompetitor, + createSource, + listSources, + updateSource, + deleteSource, + getDueSources, + getLatestSnapshot, + insertSnapshot, + insertChanges, + listChanges, + ackChange, +} from "./competitor"; + export { createScheduledJob, getScheduledJob, diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c6507e5d..c71a89eb 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -5,4 +5,5 @@ export * from "./schema/platform"; export * from "./schema/ai"; export * from "./schema/memory"; export * from "./schema/finance"; +export * from "./schema/competitor"; export * from "./schema/relations"; diff --git a/packages/db/src/schema/competitor.ts b/packages/db/src/schema/competitor.ts new file mode 100644 index 00000000..be803aec --- /dev/null +++ b/packages/db/src/schema/competitor.ts @@ -0,0 +1,139 @@ +import { + boolean, + index, + integer, + jsonb, + pgTable, + text, + timestamp, +} from "drizzle-orm/pg-core"; +import { companies } from "./tenant"; + +// ── Competitors ─────────────────────────────────────────────────────────────── + +export const competitors = pgTable( + "competitors", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + companyId: text("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + name: text("name").notNull(), + url: text("url").notNull(), + status: text("status").notNull().default("active"), // "active" | "paused" + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), + }, + (table) => [ + index("competitors_company_idx").on(table.companyId), + ] +); + +// ── Competitor Sources ──────────────────────────────────────────────────────── + +export const competitorSources = pgTable( + "competitor_sources", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + competitorId: text("competitor_id") + .notNull() + .references(() => competitors.id, { onDelete: "cascade" }), + companyId: text("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + type: text("type").notNull(), // "pricing" | "social" + url: text("url").notNull(), + config: jsonb("config"), + enabled: boolean("enabled").notNull().default(true), + intervalHours: integer("interval_hours").notNull().default(168), + lastRunAt: timestamp("last_run_at", { mode: "date" }), + lastStatus: text("last_status"), + healthState: text("health_state").notNull().default("ok"), // "ok" | "broken" + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull(), + }, + (table) => [ + index("competitor_sources_company_idx").on(table.companyId), + index("competitor_sources_competitor_idx").on(table.competitorId), + ] +); + +// ── Competitor Snapshots ────────────────────────────────────────────────────── + +export const competitorSnapshots = pgTable( + "competitor_snapshots", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + competitorId: text("competitor_id") + .notNull() + .references(() => competitors.id, { onDelete: "cascade" }), + sourceId: text("source_id") + .notNull() + .references(() => competitorSources.id, { onDelete: "cascade" }), + companyId: text("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + capturedAt: timestamp("captured_at", { mode: "date" }).defaultNow().notNull(), + raw: text("raw").notNull(), + rawHash: text("raw_hash").notNull(), + structured: jsonb("structured").notNull(), + structuredHash: text("structured_hash").notNull(), + // Tier-1 detection floor (spec §4): the normalized visible text + its hash. + // Nullable → additive, non-destructive; existing rows keep NULL and re-baseline + // once on the next run. Store-on-change now keys on normalizedHash, not rawHash. + normalized: text("normalized"), + normalizedHash: text("normalized_hash"), + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), + }, + (table) => [ + index("competitor_snapshots_source_idx").on(table.sourceId, table.capturedAt), + ] +); + +// ── Competitor Changes ──────────────────────────────────────────────────────── + +export const competitorChanges = pgTable( + "competitor_changes", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + competitorId: text("competitor_id") + .notNull() + .references(() => competitors.id, { onDelete: "cascade" }), + sourceId: text("source_id") + .notNull() + .references(() => competitorSources.id, { onDelete: "cascade" }), + snapshotId: text("snapshot_id") + .notNull() + .references(() => competitorSnapshots.id, { onDelete: "cascade" }), + companyId: text("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + detectedAt: timestamp("detected_at", { mode: "date" }).defaultNow().notNull(), + changeType: text("change_type").notNull(), + summary: text("summary").notNull(), + before: jsonb("before"), + after: jsonb("after"), + severity: text("severity").notNull().default("info"), + acknowledgedAt: timestamp("acknowledged_at", { mode: "date" }), + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), + }, + (table) => [ + index("competitor_changes_company_idx").on(table.companyId, table.detectedAt), + index("competitor_changes_competitor_idx").on(table.competitorId), + ] +); + +// ── Inferred Row Types ──────────────────────────────────────────────────────── + +export type Competitor = typeof competitors.$inferSelect; +export type CompetitorSource = typeof competitorSources.$inferSelect; +export type CompetitorSnapshot = typeof competitorSnapshots.$inferSelect; +export type CompetitorChange = typeof competitorChanges.$inferSelect; diff --git a/packages/engine/src/competitor/__tests__/diff.test.ts b/packages/engine/src/competitor/__tests__/diff.test.ts new file mode 100644 index 00000000..b27e107b --- /dev/null +++ b/packages/engine/src/competitor/__tests__/diff.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { diffStructured } from "../diff"; + +describe("diffStructured", () => { + it("returns [] for deep-equal objects", () => { + expect(diffStructured({ a: 1, b: [1, 2] }, { a: 1, b: [1, 2] })).toEqual([]); + }); + + it("detects a modified primitive with full path", () => { + const out = diffStructured({ plan: { price: 29 } }, { plan: { price: 39 } }); + expect(out).toEqual([{ kind: "modified", path: ["plan", "price"], before: 29, after: 39 }]); + }); + + it("detects added and removed keys", () => { + const out = diffStructured({ a: 1 }, { a: 1, b: 2 }); + expect(out).toContainEqual({ kind: "added", path: ["b"], after: 2 }); + const out2 = diffStructured({ a: 1, b: 2 }, { a: 1 }); + expect(out2).toContainEqual({ kind: "removed", path: ["b"], before: 2 }); + }); + + it("diffs arrays index-wise including length growth", () => { + const out = diffStructured({ plans: [{ n: "Pro" }] }, { plans: [{ n: "Pro" }, { n: "Team" }] }); + expect(out).toContainEqual({ kind: "added", path: ["plans", "1"], after: { n: "Team" } }); + }); + + it("produces deterministic ordering (sorted by path)", () => { + const out = diffStructured({ b: 1, a: 1 }, { b: 2, a: 2 }); + expect(out.map((c) => c.path.join("."))).toEqual(["a", "b"]); + }); +}); diff --git a/packages/engine/src/competitor/__tests__/normalize.test.ts b/packages/engine/src/competitor/__tests__/normalize.test.ts new file mode 100644 index 00000000..8e6d320a --- /dev/null +++ b/packages/engine/src/competitor/__tests__/normalize.test.ts @@ -0,0 +1,38 @@ +// packages/engine/src/competitor/__tests__/normalize.test.ts +import { describe, it, expect } from "vitest"; +import { normalizeHtml } from "../normalize"; + +describe("normalizeHtml", () => { + it("strips script/style/noscript/svg content", () => { + const html = `
Hi
`; + expect(normalizeHtml(html)).toBe("Hi"); + }); + + it("drops attributes so nonce/CSRF churn does not change output", () => { + const a = `

Plan $29

`; + const b = `

Plan $29

`; + expect(normalizeHtml(a)).toBe(normalizeHtml(b)); + expect(normalizeHtml(a)).toBe("Plan $29"); + }); + + it("turns block boundaries into separate lines", () => { + const html = `
  • Free
  • Pro

Enterprise

`; + expect(normalizeHtml(html)).toBe("Free\nPro\nEnterprise"); + }); + + it("collapses whitespace, decodes entities, drops empty lines", () => { + const html = `

A & B

\n\n

 

C's

`; + expect(normalizeHtml(html)).toBe("A & B\nC's"); + }); + + it("is idempotent-stable: comment-only difference yields identical text", () => { + const a = `

Same

`; + const b = `

Same

`; + expect(normalizeHtml(a)).toBe(normalizeHtml(b)); + }); + + it("returns empty string for tag-only / empty input", () => { + expect(normalizeHtml(`
`)).toBe(""); + expect(normalizeHtml("")).toBe(""); + }); +}); diff --git a/packages/engine/src/competitor/__tests__/rules.test.ts b/packages/engine/src/competitor/__tests__/rules.test.ts new file mode 100644 index 00000000..313d47c1 --- /dev/null +++ b/packages/engine/src/competitor/__tests__/rules.test.ts @@ -0,0 +1,93 @@ +// packages/engine/src/competitor/__tests__/rules.test.ts +import { describe, it, expect } from "vitest"; +import { evaluateRules } from "../rules"; +import type { Change } from "../types"; + +describe("evaluateRules — pricing", () => { + it("flags a >=10% price increase as warning with % in summary", () => { + const changes: Change[] = [ + { kind: "modified", path: ["plans", "0", "price", "amount"], before: 29, after: 39 }, + ]; + const [alert] = evaluateRules("pricing", changes); + expect(alert.changeType).toBe("price_increase"); + expect(alert.severity).toBe("warning"); + expect(alert.summary).toContain("29"); + expect(alert.summary).toContain("39"); + expect(alert.summary).toContain("34"); // ~+34% + }); + + it("flags a small price decrease as info", () => { + const changes: Change[] = [ + { kind: "modified", path: ["plans", "0", "price", "amount"], before: 100, after: 95 }, + ]; + const [alert] = evaluateRules("pricing", changes); + expect(alert.changeType).toBe("price_decrease"); + expect(alert.severity).toBe("info"); + }); + + it("flags an added plan", () => { + const changes: Change[] = [{ kind: "added", path: ["plans", "2"], after: { name: "Team" } }]; + const [alert] = evaluateRules("pricing", changes); + expect(alert.changeType).toBe("plan_added"); + }); +}); + +describe("evaluateRules — social", () => { + it("flags follower growth", () => { + const changes: Change[] = [{ kind: "modified", path: ["followers"], before: 1000, after: 2200 }]; + const [alert] = evaluateRules("social", changes); + expect(alert.changeType).toBe("followers_up"); + expect(alert.summary).toContain("1,200"); + }); +}); + +describe("evaluateRules — fallback", () => { + it("emits a generic field_changed alert for unknown paths (never drops)", () => { + const changes: Change[] = [{ kind: "modified", path: ["mystery"], before: "a", after: "b" }]; + const out = evaluateRules("pricing", changes); + expect(out).toHaveLength(1); + expect(out[0].changeType).toBe("field_changed"); + }); +}); + +describe("evaluateRules — feed", () => { + it("emits post_published for an added item, ignores removed/other", () => { + const changes = [ + { kind: "added", path: ["items", "g1"], after: { title: "Hello v2", link: "https://x/2", publishedAt: null } }, + { kind: "removed", path: ["items", "g0"], before: { title: "old" } }, + ]; + const alerts = evaluateRules("feed", changes as never); + expect(alerts).toHaveLength(1); + expect(alerts[0]).toMatchObject({ changeType: "post_published", summary: "New post: Hello v2", severity: "info" }); + }); +}); + +describe("evaluateRules — sitemap", () => { + it("emits page_added / page_removed, ignores count", () => { + const changes = [ + { kind: "added", path: ["urls", "https://x/a"], after: 1 }, + { kind: "removed", path: ["urls", "https://x/b"], before: 1 }, + { kind: "modified", path: ["count"], before: 10, after: 11 }, + ]; + const alerts = evaluateRules("sitemap", changes as never); + expect(alerts.map((a) => a.changeType).sort()).toEqual(["page_added", "page_removed"]); + expect(alerts.find((a) => a.changeType === "page_added")!.summary).toBe("Page added: https://x/a"); + }); + + it("caps a large same-type burst at 25 individual + 1 summary", () => { + const changes = Array.from({ length: 40 }, (_, i) => ({ kind: "added", path: ["urls", `https://x/${i}`], after: 1 })); + const alerts = evaluateRules("sitemap", changes as never); + const added = alerts.filter((a) => a.changeType === "page_added"); + expect(added).toHaveLength(26); // 25 individual + 1 summary + expect(added[25]!.summary).toBe("+15 pages added"); + }); +}); + +describe("evaluateRules — non-regression", () => { + it("pricing still falls back to genericAlert for unmatched changes", () => { + const changes = [{ kind: "modified", path: ["plans", "0", "name"], before: "Pro", after: "Business" }]; + const alerts = evaluateRules("pricing", changes as never); + expect(alerts).toHaveLength(1); + expect(alerts[0]!.changeType).toBe("field_changed"); // genericAlert, unchanged + }); +}); diff --git a/packages/engine/src/competitor/__tests__/text-diff.test.ts b/packages/engine/src/competitor/__tests__/text-diff.test.ts new file mode 100644 index 00000000..841c81c4 --- /dev/null +++ b/packages/engine/src/competitor/__tests__/text-diff.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { diffText, contentChangeAlert } from "../text-diff"; + +describe("diffText", () => { + it("reports added and removed lines by multiset difference", () => { + const d = diffText("Free\nPro $29\nEnterprise", "Free\nPro $39\nEnterprise\nTeam"); + expect(d.removedLines).toEqual(["Pro $29"]); + expect(d.addedLines).toEqual(["Pro $39", "Team"]); + expect(d.removedCount).toBe(1); + expect(d.addedCount).toBe(2); + expect(d.truncated).toBe(false); + }); + + it("treats identical content (any order) as no change", () => { + const d = diffText("A\nB\nC", "C\nB\nA"); + expect(d.addedCount).toBe(0); + expect(d.removedCount).toBe(0); + }); + + it("handles empty previous (first snapshot)", () => { + const d = diffText("", "Only"); + expect(d.addedLines).toEqual(["Only"]); + expect(d.removedCount).toBe(0); + }); + + it("caps displayed lines at 20 and flags truncation", () => { + const next = Array.from({ length: 25 }, (_, i) => `L${i}`).join("\n"); + const d = diffText("", next); + expect(d.addedCount).toBe(25); + expect(d.addedLines).toHaveLength(20); + expect(d.truncated).toBe(true); + }); +}); + +describe("contentChangeAlert", () => { + it("returns null when nothing changed", () => { + expect(contentChangeAlert(diffText("A", "A"), "Pricing page")).toBeNull(); + }); + + it("builds an info alert with counts and capped line snippets", () => { + const alert = contentChangeAlert(diffText("Pro 29", "Pro 39"), "Pricing page"); + expect(alert).not.toBeNull(); + expect(alert!.changeType).toBe("content_changed"); + expect(alert!.severity).toBe("info"); + expect(alert!.summary).toBe("Pricing page content changed: +1 / -1 lines"); + expect(alert!.before).toEqual({ lines: ["Pro 29"], truncated: false }); + expect(alert!.after).toEqual({ lines: ["Pro 39"], truncated: false }); + }); + + it("flags truncation per side under asymmetric change sizes", () => { + const d = diffText(Array.from({ length: 25 }, (_, i) => `R${i}`).join("\n"), "One"); + const a = contentChangeAlert(d, "X"); + expect(a).not.toBeNull(); + expect((a!.before as { truncated: boolean }).truncated).toBe(true); + expect((a!.after as { truncated: boolean }).truncated).toBe(false); + }); +}); diff --git a/packages/engine/src/competitor/diff.ts b/packages/engine/src/competitor/diff.ts new file mode 100644 index 00000000..68724401 --- /dev/null +++ b/packages/engine/src/competitor/diff.ts @@ -0,0 +1,43 @@ +import type { Change } from "./types"; + +function isObject(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + +function deepEqual(a: unknown, b: unknown): boolean { + return JSON.stringify(a) === JSON.stringify(b); +} + +function walk(prev: unknown, next: unknown, path: string[], out: Change[]): void { + if (deepEqual(prev, next)) return; + + const prevIsObj = isObject(prev); + const nextIsObj = isObject(next); + + if (!prevIsObj || !nextIsObj) { + out.push({ kind: "modified", path, before: prev, after: next }); + return; + } + + const keys = Array.from(new Set([...Object.keys(prev), ...Object.keys(next)])).sort((a, b) => + a.localeCompare(b), + ); + for (const key of keys) { + const hasPrev = key in prev; + const hasNext = key in next; + const childPath = [...path, key]; + if (hasPrev && !hasNext) { + out.push({ kind: "removed", path: childPath, before: prev[key] }); + } else if (!hasPrev && hasNext) { + out.push({ kind: "added", path: childPath, after: next[key] }); + } else { + walk(prev[key], next[key], childPath, out); + } + } +} + +export function diffStructured(prev: unknown, next: unknown): Change[] { + const out: Change[] = []; + walk(prev, next, [], out); + return out; +} diff --git a/packages/engine/src/competitor/index.ts b/packages/engine/src/competitor/index.ts new file mode 100644 index 00000000..47168326 --- /dev/null +++ b/packages/engine/src/competitor/index.ts @@ -0,0 +1,5 @@ +export * from "./types"; +export * from "./diff"; +export * from "./rules"; +export * from "./normalize"; +export * from "./text-diff"; diff --git a/packages/engine/src/competitor/normalize.ts b/packages/engine/src/competitor/normalize.ts new file mode 100644 index 00000000..7dda75b2 --- /dev/null +++ b/packages/engine/src/competitor/normalize.ts @@ -0,0 +1,51 @@ +// packages/engine/src/competitor/normalize.ts + +/** Block-level closing tags whose boundary becomes a newline in visible text. + * Built from a tag list (composed, not a single literal) to keep the pattern + * readable and below the static-analysis complexity limit while producing the + * identical regex. */ +const BLOCK_TAGS = [ + "p", "div", "li", "ul", "ol", "section", "article", "header", "footer", + "nav", "main", "aside", "table", "tr", "thead", "tbody", "h[1-6]", + "blockquote", "pre", "figure", "figcaption", "form", "dd", "dt", +].join("|"); +const BLOCK_CLOSE = new RegExp(String.raw``, "gi"); +const BR = //gi; + +function decodeEntities(s: string): string { + // Decode & LAST so we never double-decode (e.g. "&lt;" must stay "<"). + return s + .replace(/ /gi, " ") + .replace(/</gi, "<") + .replace(/>/gi, ">") + .replace(/"/gi, '"') + .replace(/�*39;/g, "'") + .replace(/'/gi, "'") + .replace(/&/gi, "&"); +} + +/** + * HTML → stable visible text (one line per visible block). Deterministic and + * pure. Same visible content with different nonces / attributes / whitespace / + * comments / inline scripts normalizes to identical output — this is what lets + * the pipeline's store-on-change ignore raw-HTML churn (spec §3.1). + */ +export function normalizeHtml(raw: string): string { + let s = raw; + // 1. Remove elements whose contents are never visible text. + s = s.replace(/<(script|style|noscript|svg|template)\b[^>]*>[\s\S]*?<\/\1>/gi, " "); + // 2. Remove HTML comments. + s = s.replace(//g, " "); + // 3. Block boundaries → newline (preserve line structure for the line-diff). + s = s.replace(BR, "\n").replace(BLOCK_CLOSE, "\n"); + // 4. Strip every remaining tag (removes all attributes → nonce/CSRF/data-* gone). + s = s.replace(/<[^>]*>?/g, " "); + // 5. Decode common entities. + s = decodeEntities(s); + // 6. Per line: collapse intra-line whitespace, trim, drop empties. + return s + .split("\n") + .map((line) => line.replace(/[^\S\n]+/g, " ").trim()) + .filter((line) => line.length > 0) + .join("\n"); +} diff --git a/packages/engine/src/competitor/rules.ts b/packages/engine/src/competitor/rules.ts new file mode 100644 index 00000000..f47e5024 --- /dev/null +++ b/packages/engine/src/competitor/rules.ts @@ -0,0 +1,145 @@ +import { D, dRound2 } from "../decimal"; +import type { Alert, Change, Severity } from "./types"; + +function num(v: unknown): number | null { + return typeof v === "number" && Number.isFinite(v) ? v : null; +} + +function fmtInt(n: number): string { + return n.toLocaleString("en-US"); +} + +function priceAmountAlert(c: Change): Alert | null { + const before = num(c.before); + const after = num(c.after); + if (before === null || after === null || before <= 0) return null; + const pct = dRound2(D(after).minus(before).div(before).times(100)); + const up = after > before; + const sev: Severity = Math.abs(pct) >= 10 ? "warning" : "info"; + return { + changeType: up ? "price_increase" : "price_decrease", + summary: `Plan price ${before} → ${after} (${up ? "+" : ""}${pct}%)`, + severity: sev, + before, + after, + }; +} + +function pricingAlert(c: Change): Alert | null { + const last = c.path.at(-1); + const isPlanRoot = c.path[0] === "plans" && c.path.length === 2; + if (c.kind === "added" && isPlanRoot) { + return { changeType: "plan_added", summary: "New pricing plan added", severity: "info", after: c.after }; + } + if (c.kind === "removed" && isPlanRoot) { + return { changeType: "plan_removed", summary: "Pricing plan removed", severity: "info", before: c.before }; + } + if (c.kind === "modified" && last === "amount") { + return priceAmountAlert(c); + } + return null; +} + +function socialAlert(c: Change): Alert | null { + if (c.kind === "modified" && c.path.at(-1) === "followers") { + const before = num(c.before); + const after = num(c.after); + if (before !== null && after !== null) { + const delta = after - before; + const up = delta >= 0; + return { + changeType: up ? "followers_up" : "followers_down", + summary: `${up ? "+" : ""}${fmtInt(delta)} followers (${fmtInt(before)} → ${fmtInt(after)})`, + severity: "info", + before, + after, + }; + } + } + return null; +} + +function genericAlert(c: Change): Alert { + return { + changeType: "field_changed", + summary: `${c.kind} at ${c.path.join(".") || "(root)"}`, + severity: "info", + before: c.before, + after: c.after, + }; +} + +const MAX_ALERTS_PER_RUN = 25; + +function feedAlert(c: Change): Alert | null { + if (c.kind === "added" && c.path[0] === "items" && c.path.length === 2) { + const after = c.after as { title?: string } | undefined; + return { + changeType: "post_published", + summary: `New post: ${after?.title ?? "(untitled)"}`, + severity: "info", + after: c.after, + }; + } + return null; // removed items (windowed feed) + nested mods → dropped +} + +function sitemapAlert(c: Change): Alert | null { + if (c.path[0] === "urls" && c.path.length === 2) { + const url = c.path[1]; + if (c.kind === "added") { + return { changeType: "page_added", summary: `Page added: ${url}`, severity: "info", after: c.after }; + } + if (c.kind === "removed") { + return { changeType: "page_removed", summary: `Page removed: ${url}`, severity: "info", before: c.before }; + } + } + return null; // count modification + truncated marker → dropped +} + +function dropNull(changes: Change[], rule: (c: Change) => Alert | null): Alert[] { + const out: Alert[] = []; + for (const c of changes) { + const a = rule(c); + if (a !== null) out.push(a); + } + return out; +} + +function overflowNoun(changeType: string): string { + switch (changeType) { + case "page_added": + return "pages added"; + case "page_removed": + return "pages removed"; + case "post_published": + return "posts published"; + default: + return "changes"; + } +} + +/** Anti-flood: keep ≤25 individual alerts per changeType, collapse the rest into + * one summary alert for that type. Order-stable. */ +function capAlerts(alerts: Alert[]): Alert[] { + const kept: Alert[] = []; + const seen = new Map(); + const overflow = new Map(); + for (const a of alerts) { + const n = (seen.get(a.changeType) ?? 0) + 1; + seen.set(a.changeType, n); + if (n <= MAX_ALERTS_PER_RUN) kept.push(a); + else overflow.set(a.changeType, (overflow.get(a.changeType) ?? 0) + 1); + } + for (const [changeType, n] of overflow) { + kept.push({ changeType, summary: `+${n} ${overflowNoun(changeType)}`, severity: "info" }); + } + return kept; +} + +export function evaluateRules(type: string, changes: Change[]): Alert[] { + if (type === "feed") return capAlerts(dropNull(changes, feedAlert)); + if (type === "sitemap") return capAlerts(dropNull(changes, sitemapAlert)); + const specific = type === "pricing" ? pricingAlert : type === "social" ? socialAlert : null; + return changes.map((c) => specific?.(c) ?? genericAlert(c)); +} diff --git a/packages/engine/src/competitor/text-diff.ts b/packages/engine/src/competitor/text-diff.ts new file mode 100644 index 00000000..ac816ffb --- /dev/null +++ b/packages/engine/src/competitor/text-diff.ts @@ -0,0 +1,66 @@ +import type { Alert } from "./types"; + +export interface TextDiff { + addedLines: string[]; // capped to CAP + removedLines: string[]; // capped to CAP + addedCount: number; // uncapped + removedCount: number; // uncapped + truncated: boolean; +} + +const CAP = 20; + +function counts(lines: string[]): Map { + const m = new Map(); + for (const l of lines) m.set(l, (m.get(l) ?? 0) + 1); + return m; +} + +/** + * Multiset line difference over normalized visible text. Order-insensitive and + * O(n): a line moved but otherwise unchanged registers as no change. Returns + * what text appeared (added) / disappeared (removed) — the useful monitoring + * signal — with displayed lists capped at CAP (spec §3.2). + */ +export function diffText(prev: string, next: string): TextDiff { + const prevLines = prev ? prev.split("\n") : []; + const nextLines = next ? next.split("\n") : []; + const prevCounts = counts(prevLines); + const nextCounts = counts(nextLines); + + const added: string[] = []; + for (const [line, n] of nextCounts) { + const surplus = n - (prevCounts.get(line) ?? 0); + for (let i = 0; i < surplus; i++) added.push(line); + } + const removed: string[] = []; + for (const [line, p] of prevCounts) { + const surplus = p - (nextCounts.get(line) ?? 0); + for (let i = 0; i < surplus; i++) removed.push(line); + } + + return { + addedLines: added.slice(0, CAP), + removedLines: removed.slice(0, CAP), + addedCount: added.length, + removedCount: removed.length, + truncated: added.length > CAP || removed.length > CAP, + }; +} + +/** + * Tier-1 generic content-change alert. `label` is a human page label supplied + * by the caller (e.g. "Pricing page"). Returns null when the diff is empty so + * the pipeline emits no change. Kept as a dedicated helper (not folded into + * evaluateRules) because a content change is not a structured `Change`. + */ +export function contentChangeAlert(diff: TextDiff, label: string): Alert | null { + if (diff.addedCount === 0 && diff.removedCount === 0) return null; + return { + changeType: "content_changed", + summary: `${label} content changed: +${diff.addedCount} / -${diff.removedCount} lines`, + severity: "info", + before: { lines: diff.removedLines, truncated: diff.removedLines.length < diff.removedCount }, + after: { lines: diff.addedLines, truncated: diff.addedLines.length < diff.addedCount }, + }; +} diff --git a/packages/engine/src/competitor/types.ts b/packages/engine/src/competitor/types.ts new file mode 100644 index 00000000..c1d11ed2 --- /dev/null +++ b/packages/engine/src/competitor/types.ts @@ -0,0 +1,17 @@ +export type Severity = "info" | "success" | "warning" | "error"; +export type StructuredPayload = Record; + +export interface Change { + kind: "added" | "removed" | "modified"; + path: string[]; + before?: unknown; + after?: unknown; +} + +export interface Alert { + changeType: string; + summary: string; + severity: Severity; + before?: unknown; + after?: unknown; +} diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 6f8420ea..a2ee5787 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -247,6 +247,9 @@ export type { NormalizedWebhookEvent, } from "./payments"; +// Competitor analysis — diff, rules, alert layer +export * from "./competitor"; + // Bank connectors — import directly from "@burnless/engine/bank-connectors" when needed. // Not re-exported here to avoid bundling optional plaid SDK. export type {