From 29edb2d4fb4963281b8d878db3ba231e63244ab8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 29 Oct 2026 11:05:11 -0600 Subject: [PATCH 01/15] Protect live presence intake and isolate retention cleanup --- docs/live-visitors.md | 22 +++++- src/components/admin/LiveVisitors.tsx | 1 + src/db/schema.ts | 9 ++- src/server/runtime/host.server.ts | 6 ++ src/server/scheduled.server.ts | 15 +++- src/utils/analytics/live.server.ts | 54 +++++++++++--- src/utils/analytics/live.ts | 2 +- src/utils/analytics/scarf-lookup.server.ts | 2 + tests/live-intake.test.ts | 76 +++++++++++++++++-- tests/live-lookup-limit.test.ts | 87 ++++++++++++++++++++++ tests/live-presence.test.ts | 14 ++++ tests/live-scheduled.test.ts | 51 +++++++++++++ wrangler.jsonc | 12 +++ 13 files changed, 329 insertions(+), 22 deletions(-) create mode 100644 tests/live-lookup-limit.test.ts create mode 100644 tests/live-scheduled.test.ts diff --git a/docs/live-visitors.md b/docs/live-visitors.md index 8c61d9046..f3016aa60 100644 --- a/docs/live-visitors.md +++ b/docs/live-visitors.md @@ -82,17 +82,35 @@ DNT and GPC prevent collection in both the browser and server. Browser collectio is disabled in development, staging, and outside `tanstack.com`. `isLivePage` allows public library content and selected public site routes. +Showcase submission, editing, and individual detail pages are excluded because +detail pages can include owner-only drafts. Existing records with excluded paths +are filtered out of admin responses and expire through normal retention. Account, admin, auth, chat, private builder projects, shop, and internal endpoints are excluded. Unknown paths are excluded until added to the public allowlist. Queries, fragments, clipboard text, form values, event properties, account IDs, emails, and user agents are not stored. Only explicitly named actions from the existing analytics hooks are collected; there is no arbitrary click-text capture. -Visitor IPs exist only during the server-side lookup and are not written to the -presence tables or admin responses. Lookup tracing and Sentry breadcrumbs are +Visitor IPs are used transiently for the server-side lookup and Cloudflare edge +rate-limit counters, and are not written to the application database or admin +responses. Lookup tracing and Sentry breadcrumbs are suppressed to keep IP query parameters out of telemetry. The lookup key stays server-side. There is no joining sessions by IP, fingerprint, or login. +## Intake limits + +Cloudflare edge bindings limit intake to 600 requests per IP per minute and new +company lookups to 30 per IP per minute, per Cloudflare location. These are +approximate abuse limits, not a global billing quota. Intake returns 429 before +reading a body or writing presence when limited, and 503 if the limiter is +unavailable. A lookup limit leaves presence intact with a separate rate-limited +status. Corporate networks share an IP, so these limits can affect shared +networks during large bursts. Raw IPs are not added to the database-backed MCP +rate-limit table. Presence cleanup runs independently of workflow processing, so +a cleanup failure does not prevent scheduled workflow work. + +[Cloudflare rate-limit bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) + ## Validation Focused tests cover privacy signals, intake validation, public-route boundaries, diff --git a/src/components/admin/LiveVisitors.tsx b/src/components/admin/LiveVisitors.tsx index 47eb27b0b..f71d0a06b 100644 --- a/src/components/admin/LiveVisitors.tsx +++ b/src/components/admin/LiveVisitors.tsx @@ -67,6 +67,7 @@ const lookupLabels: Record = { pending: 'Looking up company', unknown: 'No cached company match', failed: 'Company lookup failed', + rate_limited: 'Company lookup rate limited', unconfigured: 'Company lookup not configured', } function age(time: Date | string | number, now: string) { diff --git a/src/db/schema.ts b/src/db/schema.ts index 54a0fd536..bba2b3d84 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -3470,7 +3470,14 @@ export const liveSessions = pgTable( company: jsonb('company').$type(), lookupStatus: text('lookup_status', { - enum: ['pending', 'matched', 'unknown', 'failed', 'unconfigured'], + enum: [ + 'pending', + 'matched', + 'unknown', + 'failed', + 'unconfigured', + 'rate_limited', + ], }).notNull(), }, (table) => [index('live_sessions_created_at_idx').on(table.createdAt)], diff --git a/src/server/runtime/host.server.ts b/src/server/runtime/host.server.ts index 6d5990be3..e22b3338e 100644 --- a/src/server/runtime/host.server.ts +++ b/src/server/runtime/host.server.ts @@ -5,7 +5,13 @@ type StaticAssetService = { fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise } +export type HostRateLimiter = { + limit(options: { key: string }): Promise<{ success: boolean }> +} + type HostRuntimeEnv = Record & { + LIVE_PRESENCE_RATE_LIMITER?: HostRateLimiter + LIVE_LOOKUP_RATE_LIMITER?: HostRateLimiter CLOUDFLARE_CACHE_PURGE_TOKEN?: string CLOUDFLARE_ZONE_ID?: string GITHUB_AUTH_TOKEN?: string diff --git a/src/server/scheduled.server.ts b/src/server/scheduled.server.ts index 54e60d31d..c1b55aa49 100644 --- a/src/server/scheduled.server.ts +++ b/src/server/scheduled.server.ts @@ -34,19 +34,30 @@ export async function runScheduledTasks(cron: string, scheduledTime: number) { ]) return case WORKFLOW_SWEEP_CRON: - await runWorkflowSweep(cron, scheduledTime) + await Promise.all([ + runWorkflowSweep(cron, scheduledTime), + runLivePresencePrune(scheduledTime), + ]) return default: console.warn(`[scheduled] No task registered for cron: ${cron}`) } } +async function runLivePresencePrune(scheduledTime: number) { + const startTime = Date.now() + try { + await pruneLiveVisitors(scheduledTime) + } catch (error) { + logScheduledError('live-presence-prune', startTime, error) + } +} + async function runWorkflowSweep(cron: string, scheduledTime: number) { const startTime = Date.now() console.log('[workflow-sweep] Starting workflow sweep...') try { - await pruneLiveVisitors(scheduledTime) const interruptedBuilderRuns = await interruptExpiredBuilderProjectRunLeases({ occurredAt: new Date(scheduledTime), diff --git a/src/utils/analytics/live.server.ts b/src/utils/analytics/live.server.ts index eda58e50f..29d73d827 100644 --- a/src/utils/analytics/live.server.ts +++ b/src/utils/analytics/live.server.ts @@ -3,9 +3,13 @@ import { isIP } from 'node:net' import { db } from '~/db/client' import { liveSessions, liveTabs } from '~/db/schema' import { requireAdmin } from '~/utils/roles.server' -import { scheduleHostRuntimeTask } from '~/server/runtime/host.server' +import { + scheduleHostRuntimeTask, + type HostRateLimiter, +} from '~/server/runtime/host.server' import { getLiveConfig, lookupScarfCompany } from './scarf-lookup.server' import { + isLivePage, liveEventSchema, LIVE_RETENTION_MS, LIVE_TIMEOUT_MS, @@ -33,6 +37,18 @@ export async function receiveLiveEvent(request: Request) { if (!ip || !isIP(ip)) return respond(204) const config = await getLiveConfig() if (!config.enabled) return respond(204) + // Edge counters bound abuse without writing visitor IPs to the database. + if (!config.requestLimiter || (config.key && !config.lookupLimiter)) + return respond(503) + try { + if (!(await config.requestLimiter.limit({ key: ip })).success) + return new Response(null, { + status: 429, + headers: { ...responseHeaders, 'Retry-After': '60' }, + }) + } catch { + return respond(503) + } const reader = request.body?.getReader() if (!reader) return respond(400) const decoder = new TextDecoder() @@ -58,7 +74,7 @@ export async function receiveLiveEvent(request: Request) { const parsed = liveEventSchema.safeParse(value) if (!parsed.success) return respond(400) try { - await recordLiveEvent(parsed.data, ip, config.key) + await recordLiveEvent(parsed.data, ip, config.key, config.lookupLimiter) return respond(204) } catch { // Never log a request, body, IP, or third-party exception here. @@ -71,6 +87,7 @@ export async function recordLiveEvent( event: LiveEvent, ip: string, key: string | undefined, + lookupLimiter: HostRateLimiter | undefined, ) { const now = new Date() if (event.kind === 'leave') { @@ -127,6 +144,16 @@ export async function recordLiveEvent( if (created && key) { const enrich = async () => { try { + if ( + !lookupLimiter || + !(await lookupLimiter.limit({ key: ip })).success + ) { + await db + .update(liveSessions) + .set({ lookupStatus: 'rate_limited' }) + .where(eq(liveSessions.id, event.sessionId)) + return + } const company = await lookupScarfCompany(ip, key) await db .update(liveSessions) @@ -183,15 +210,20 @@ export async function listLiveVisitors() { lookupConfigured: Boolean(config.key), now: new Date(now).toISOString(), truncated: rows.length > 500, - tabs: rows.slice(0, 500).map((row) => ({ - ...row, - active: - row.leftAt === null && now - row.lastSeen.getTime() < LIVE_TIMEOUT_MS, - lookupStatus: - row.lookupStatus === 'pending' && now - row.arrivedAt.getTime() > 15_000 - ? 'failed' - : row.lookupStatus, - })), + tabs: rows + .slice(0, 500) + .filter((row) => isLivePage(row.page)) + .map((row) => ({ + ...row, + activity: row.activity.filter((event) => isLivePage(event.page)), + active: + row.leftAt === null && now - row.lastSeen.getTime() < LIVE_TIMEOUT_MS, + lookupStatus: + row.lookupStatus === 'pending' && + now - row.arrivedAt.getTime() > 15_000 + ? 'failed' + : row.lookupStatus, + })), } } diff --git a/src/utils/analytics/live.ts b/src/utils/analytics/live.ts index 8fe61dbb7..230884ea4 100644 --- a/src/utils/analytics/live.ts +++ b/src/utils/analytics/live.ts @@ -30,7 +30,7 @@ export const liveActionSchema = z.enum([ export function isLivePage(page: string) { return ( page.length <= 400 && - /^\/(?:$|(?:query|router|start|table|db|form|store|virtual|pacer|hotkeys|ai|charts|intent|devtools|config|cli|highlight|markdown|ranger|redact|mcp|workflow)(?:\/|$)|(?:blog|partners|application-starter|libraries|stack|stats|showcase)(?:\/|$)|(?:brand-guide|community-projects|ethos|explore|feedback-leaderboard|learn|maintainers|paid-support|privacy|showcases|support|tenets|terms|workshops)\/?$)/.test( + /^\/(?:$|(?:query|router|start|table|db|form|store|virtual|pacer|hotkeys|ai|charts|intent|devtools|config|cli|highlight|markdown|ranger|redact|mcp|workflow)(?:\/|$)|(?:blog|partners|application-starter|libraries|stack|stats)(?:\/|$)|(?:brand-guide|community-projects|ethos|explore|feedback-leaderboard|learn|maintainers|paid-support|privacy|showcase|showcases|support|tenets|terms|workshops)\/?$)/.test( page, ) && !/[?#\\\s%]/.test(page) && diff --git a/src/utils/analytics/scarf-lookup.server.ts b/src/utils/analytics/scarf-lookup.server.ts index 4e6c43fb9..50a04e2a7 100644 --- a/src/utils/analytics/scarf-lookup.server.ts +++ b/src/utils/analytics/scarf-lookup.server.ts @@ -7,6 +7,8 @@ export async function getLiveConfig() { const host = await getHostRuntimeEnv() const key = host?.SCARF_LOOKUP_KEY ?? process.env.SCARF_LOOKUP_KEY return { + requestLimiter: host?.LIVE_PRESENCE_RATE_LIMITER, + lookupLimiter: host?.LIVE_LOOKUP_RATE_LIMITER, enabled: (host?.LIVE_VISITORS_ENABLED ?? process.env.LIVE_VISITORS_ENABLED) === 'true', diff --git a/tests/live-intake.test.ts b/tests/live-intake.test.ts index 6832c3b37..976daa96e 100644 --- a/tests/live-intake.test.ts +++ b/tests/live-intake.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict' import { test } from 'node:test' import { receiveLiveEvent } from '../src/utils/analytics/live.server.ts' +import { runWithHostRuntimeEnv } from '../src/server/runtime/host.server.ts' function request(body = '{}', headers: Record = {}) { return new Request('https://tanstack.com/_a/live', { @@ -42,16 +43,81 @@ test('enabled intake bounds and validates data before using the database', async if (previous === undefined) delete process.env.LIVE_VISITORS_ENABLED else process.env.LIVE_VISITORS_ENABLED = previous }) - assert.equal((await receiveLiveEvent(request('x'.repeat(1025)))).status, 413) - assert.equal((await receiveLiveEvent(request('invalid'))).status, 400) + assert.equal((await allowedIntake(request('x'.repeat(1025)))).status, 413) + assert.equal((await allowedIntake(request('invalid'))).status, 400) assert.equal( - (await receiveLiveEvent(request(JSON.stringify({ page: '/admin' })))) - .status, + (await allowedIntake(request(JSON.stringify({ page: '/admin' })))).status, 400, ) assert.equal( - (await receiveLiveEvent(request('{}', { 'cf-connecting-ip': 'not-an-ip' }))) + (await allowedIntake(request('{}', { 'cf-connecting-ip': 'not-an-ip' }))) .status, 204, ) }) + +function allowedIntake(incoming: Request) { + return runWithHostRuntimeEnv( + { + LIVE_PRESENCE_RATE_LIMITER: { limit: async () => ({ success: true }) }, + LIVE_LOOKUP_RATE_LIMITER: { limit: async () => ({ success: true }) }, + }, + () => receiveLiveEvent(incoming), + ) +} + +test('edge throttling rejects requests before reading bodies or using the database', async (t) => { + const previous = process.env.LIVE_VISITORS_ENABLED + process.env.LIVE_VISITORS_ENABLED = 'true' + t.after(() => { + if (previous === undefined) delete process.env.LIVE_VISITORS_ENABLED + else process.env.LIVE_VISITORS_ENABLED = previous + }) + const incoming = request('not JSON') + const response = await runWithHostRuntimeEnv( + { + LIVE_PRESENCE_RATE_LIMITER: { + limit: async ({ key }: { key: string }) => { + assert.equal(key, '192.0.2.1') + return { success: false } + }, + }, + LIVE_LOOKUP_RATE_LIMITER: { limit: async () => ({ success: true }) }, + }, + () => receiveLiveEvent(incoming), + ) + assert.equal(response.status, 429) + assert.equal(response.headers.get('Retry-After'), '60') + assert.equal(incoming.bodyUsed, false) +}) + +test('enabled intake fails closed when its edge limiter is unavailable', async (t) => { + const previous = process.env.LIVE_VISITORS_ENABLED + process.env.LIVE_VISITORS_ENABLED = 'true' + t.after(() => { + if (previous === undefined) delete process.env.LIVE_VISITORS_ENABLED + else process.env.LIVE_VISITORS_ENABLED = previous + }) + assert.equal( + (await runWithHostRuntimeEnv({}, () => receiveLiveEvent(request()))).status, + 503, + ) + const incoming = request('not JSON') + assert.equal( + ( + await runWithHostRuntimeEnv( + { + LIVE_PRESENCE_RATE_LIMITER: { + limit: async () => { + throw new Error('Unavailable') + }, + }, + LIVE_LOOKUP_RATE_LIMITER: { limit: async () => ({ success: true }) }, + }, + () => receiveLiveEvent(incoming), + ) + ).status, + 503, + ) + assert.equal(incoming.bodyUsed, false) +}) diff --git a/tests/live-lookup-limit.test.ts b/tests/live-lookup-limit.test.ts new file mode 100644 index 000000000..4ac08c246 --- /dev/null +++ b/tests/live-lookup-limit.test.ts @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) + +const bundle = build({ + entryPoints: ['src/utils/analytics/live.server.ts'], + bundle: true, + write: false, + platform: 'node', + format: 'iife', + globalName: 'live', + plugins: [ + { + name: 'lookup-dependencies', + setup(build) { + build.onResolve( + { + filter: + /^(~\/db\/client|~\/utils\/roles.server|~\/server\/runtime\/host.server|\.\/scarf-lookup.server)$/, + }, + (args) => ({ path: args.path, namespace: 'live-test' }), + ) + build.onLoad({ filter: /.*/, namespace: 'live-test' }, (args) => ({ + contents: + args.path === '~/db/client' + ? ` + export const db = { + insert() { return { values() { return { onConflictDoNothing() { return { async returning() { return [{ id: 'session' }] } } }, async onConflictDoUpdate() {} } } } }, + update() { return { set(value) { updates.push(value); return { async where() {} } } } }, + } + ` + : args.path === './scarf-lookup.server' + ? ` + export async function getLiveConfig() { return {} } + export async function lookupScarfCompany() { calls.push('lookup'); return null } + ` + : args.path === '~/utils/roles.server' + ? 'export async function requireAdmin() {}' + : 'export function scheduleHostRuntimeTask() { return false }', + })) + }, + }, + ], +}) + +test('new sessions do not call Scarf when the separate lookup budget is exhausted', async () => { + const calls: string[] = [] + const updates: unknown[] = [] + const result = await bundle + const api = runInNewContext(`${result.outputFiles[0].text}\nlive`, { + require, + calls, + updates, + console, + TextEncoder, + TextDecoder, + URL, + setTimeout, + clearTimeout, + }) + await api.recordLiveEvent( + { + sessionId: 'session', + tabId: 'tab', + sequence: 1, + page: '/query/latest', + visible: true, + kind: 'update', + }, + '192.0.2.1', + 'test-key', + { + async limit({ key }: { key: string }) { + assert.equal(key, '192.0.2.1') + return { success: false } + }, + }, + ) + assert.deepEqual(calls, []) + assert.equal( + JSON.stringify(updates), + JSON.stringify([{ lookupStatus: 'rate_limited' }]), + ) +}) diff --git a/tests/live-presence.test.ts b/tests/live-presence.test.ts index e4ecfb6ab..e66f89d7b 100644 --- a/tests/live-presence.test.ts +++ b/tests/live-presence.test.ts @@ -197,3 +197,17 @@ test('an expired session rotates its tab too, and restored pages rejoin', async assert.notEqual(last?.tabId, first.tabId) visitor.stop() }) + +test('showcase collection excludes drafts, submission, and editing paths', () => { + assert.equal(isLivePage('/showcase'), true) + assert.equal(isLivePage('/showcase/'), true) + assert.equal(isLivePage('/showcases'), true) + for (const page of [ + '/showcase/submit', + '/showcase/submit/', + '/showcase/edit/123', + '/showcase/491fa786-94d5-4d42-8c54-df45c7a7c066', + ]) { + assert.equal(isLivePage(page), false, page) + } +}) diff --git a/tests/live-scheduled.test.ts b/tests/live-scheduled.test.ts new file mode 100644 index 000000000..2f91ef710 --- /dev/null +++ b/tests/live-scheduled.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +test('failed presence cleanup does not interrupt scheduled workflow processing', async () => { + const calls: string[] = [] + const bundle = await build({ + entryPoints: ['src/server/scheduled.server.ts'], + bundle: true, + write: false, + format: 'iife', + globalName: 'scheduled', + plugins: [ + { + name: 'scheduled-dependencies', + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => + args.importer ? { path: args.path, namespace: 'stub' } : undefined, + ) + build.onLoad({ filter: /.*/, namespace: 'stub' }, () => ({ + contents: ` + export async function pruneLiveVisitors() { calls.push('presence-prune'); throw new Error('Database unavailable') } + export async function interruptExpiredBuilderProjectRunLeases() { calls.push('builder-leases'); return 0 } + export async function reconcileBuilderProjectSnapshotReservations() { return {} } + export async function pruneBuilderProjectSnapshotStorage() { return {} } + export async function pruneDeletedBuilderProjects() { return 0 } + export async function reconcileWorkflowRuntimeStore() { return {} } + export async function materializeWorkflowSchedules() { calls.push('materialize'); return [] } + export const workflowRuntime = { async sweep() { calls.push('workflow-sweep'); return { summary: {} } } } + export const WORKFLOW_RUNTIME_MAX_DURATION_MS = 1000 + export const WORKFLOW_RUNTIME_MIN_REMAINING_MS = 100 + export async function pruneStaleCacheRows() {} + export async function refreshHomepageNpmStatsSummary() {} + export async function refreshGitHubOrgStats() {} + `, + })) + }, + }, + ], + }) + const api = runInNewContext(`${bundle.outputFiles[0].text}\nscheduled`, { + calls, + console: { log() {}, warn() {}, error() {} }, + }) + await api.runScheduledTasks('* * * * *', Date.now()) + assert.ok(calls.includes('presence-prune')) + assert.ok(calls.includes('builder-leases')) + assert.ok(calls.includes('materialize')) + assert.ok(calls.includes('workflow-sweep')) +}) diff --git a/wrangler.jsonc b/wrangler.jsonc index 424946cfc..b0fcb35e3 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -18,6 +18,18 @@ "KODY_ORIGIN": "https://kody.codes", "INCLUDED_MODEL": "@cf/moonshotai/kimi-k2.6", }, + "ratelimits": [ + { + "name": "LIVE_PRESENCE_RATE_LIMITER", + "namespace_id": "144201", + "simple": { "limit": 600, "period": 60 }, + }, + { + "name": "LIVE_LOOKUP_RATE_LIMITER", + "namespace_id": "144202", + "simple": { "limit": 30, "period": 60 }, + }, + ], "limits": { "cpu_ms": 300000, }, From 2074b83f5594dd57fb053c7a8834a7ef00c970bc Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 29 Oct 2026 11:24:05 -0600 Subject: [PATCH 02/15] Apply live page eligibility before limiting results --- src/utils/analytics/live.server.ts | 29 +++++++++++++++-------------- src/utils/analytics/live.ts | 11 ++++++----- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/utils/analytics/live.server.ts b/src/utils/analytics/live.server.ts index 29d73d827..0bd95c553 100644 --- a/src/utils/analytics/live.server.ts +++ b/src/utils/analytics/live.server.ts @@ -10,6 +10,8 @@ import { import { getLiveConfig, lookupScarfCompany } from './scarf-lookup.server' import { isLivePage, + livePublicPagePattern, + liveForbiddenPagePattern, liveEventSchema, LIVE_RETENTION_MS, LIVE_TIMEOUT_MS, @@ -201,6 +203,9 @@ export async function listLiveVisitors() { and( gt(liveTabs.lastSeen, new Date(now - 5 * 60_000)), gt(liveSessions.createdAt, new Date(now - LIVE_RETENTION_MS)), + sql`length(${liveTabs.page}) <= 400`, + sql`${liveTabs.page} ~ ${livePublicPagePattern.source}`, + sql`${liveTabs.page} !~ ${liveForbiddenPagePattern.source}`, ), ) .orderBy(desc(liveTabs.lastSeen)) @@ -210,20 +215,16 @@ export async function listLiveVisitors() { lookupConfigured: Boolean(config.key), now: new Date(now).toISOString(), truncated: rows.length > 500, - tabs: rows - .slice(0, 500) - .filter((row) => isLivePage(row.page)) - .map((row) => ({ - ...row, - activity: row.activity.filter((event) => isLivePage(event.page)), - active: - row.leftAt === null && now - row.lastSeen.getTime() < LIVE_TIMEOUT_MS, - lookupStatus: - row.lookupStatus === 'pending' && - now - row.arrivedAt.getTime() > 15_000 - ? 'failed' - : row.lookupStatus, - })), + tabs: rows.slice(0, 500).map((row) => ({ + ...row, + activity: row.activity.filter((event) => isLivePage(event.page)), + active: + row.leftAt === null && now - row.lastSeen.getTime() < LIVE_TIMEOUT_MS, + lookupStatus: + row.lookupStatus === 'pending' && now - row.arrivedAt.getTime() > 15_000 + ? 'failed' + : row.lookupStatus, + })), } } diff --git a/src/utils/analytics/live.ts b/src/utils/analytics/live.ts index 230884ea4..b34f1b897 100644 --- a/src/utils/analytics/live.ts +++ b/src/utils/analytics/live.ts @@ -27,14 +27,15 @@ export const liveActionSchema = z.enum([ ]) // Only public content paths. Never store a query, fragment, or private route. +export const livePublicPagePattern = + /^\/(?:$|(?:query|router|start|table|db|form|store|virtual|pacer|hotkeys|ai|charts|intent|devtools|config|cli|highlight|markdown|ranger|redact|mcp|workflow)(?:\/|$)|(?:blog|partners|application-starter|libraries|stack|stats)(?:\/|$)|(?:brand-guide|community-projects|ethos|explore|feedback-leaderboard|learn|maintainers|paid-support|privacy|showcase|showcases|support|tenets|terms|workshops)\/?$)/ +export const liveForbiddenPagePattern = /[?#\\\s%]|\.\./ + export function isLivePage(page: string) { return ( page.length <= 400 && - /^\/(?:$|(?:query|router|start|table|db|form|store|virtual|pacer|hotkeys|ai|charts|intent|devtools|config|cli|highlight|markdown|ranger|redact|mcp|workflow)(?:\/|$)|(?:blog|partners|application-starter|libraries|stack|stats)(?:\/|$)|(?:brand-guide|community-projects|ethos|explore|feedback-leaderboard|learn|maintainers|paid-support|privacy|showcase|showcases|support|tenets|terms|workshops)\/?$)/.test( - page, - ) && - !/[?#\\\s%]/.test(page) && - !page.includes('..') + livePublicPagePattern.test(page) && + !liveForbiddenPagePattern.test(page) ) } From 57a6fc8c97891091d04bab5e9ee7f1422b37c517 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 29 Oct 2026 12:05:20 -0600 Subject: [PATCH 03/15] Add company prioritization tools and Scarf history to live dashboard --- docs/live-visitors.md | 45 +- src/components/ProductScarf.tsx | 9 +- src/components/admin/CompanyFacetFilter.tsx | 64 + src/components/admin/LiveCompanyCharts.tsx | 182 +-- src/components/admin/LiveCompanySessions.tsx | 175 +++ src/components/admin/LiveVisitors.tsx | 1375 +++++++++++++----- src/routes/admin/live.tsx | 80 +- src/routes/index.tsx | 3 +- src/server/runtime/blob-storage.server.ts | 2 +- src/utils/analytics/live-dashboard.ts | 164 +++ src/utils/analytics/live.functions.ts | 26 +- src/utils/analytics/live.server.ts | 73 +- src/utils/analytics/scarf-history.server.ts | 191 +++ src/utils/analytics/scarf-history.ts | 96 ++ src/utils/analytics/scarf-pixels.ts | 7 + tests/live-company-tools.test.ts | 197 +++ tests/scarf-history.test.ts | 95 ++ 17 files changed, 2242 insertions(+), 542 deletions(-) create mode 100644 src/components/admin/CompanyFacetFilter.tsx create mode 100644 src/components/admin/LiveCompanySessions.tsx create mode 100644 src/utils/analytics/scarf-history.server.ts create mode 100644 src/utils/analytics/scarf-history.ts create mode 100644 src/utils/analytics/scarf-pixels.ts create mode 100644 tests/live-company-tools.test.ts create mode 100644 tests/scarf-history.test.ts diff --git a/docs/live-visitors.md b/docs/live-visitors.md index f3016aa60..c9ad5c002 100644 --- a/docs/live-visitors.md +++ b/docs/live-visitors.md @@ -7,14 +7,41 @@ function. Admin queries refresh every three seconds while the page is visible. The dashboard opens with a company grid. Individual sessions are hidden until a company is expanded, and each session has its own expandable tabs and activity. Unmatched traffic stays in one collapsed row at the bottom. Columns support -sorting, and search matches companies, industries, libraries, and page paths. +multi-column sorting with Shift-click, column visibility, row selection, +pagination, compact density, and CSV export. Search matches companies, industries, +libraries, and page paths. Library, industry, size, and funnel-stage filters can +be combined with minimum website views and recent actions. Values within a +filter match with OR, while different filters match with AND. The overview counts identified companies once per domain. Library breakdowns count a company once per library, so one company can appear in several bars. -Industry breakdowns exclude unmatched traffic. The company trend records real -polling snapshots in the open dashboard tab, up to five minutes, and resets on -reload. Grid action totals count named interactions in the last five minutes, -excluding page views. +Industry and size breakdowns exclude unmatched traffic. Clicking a breakdown +applies its filter. Size buckets use the lower bound of Scarf's employee range. +Grid action totals count named interactions in the last five minutes, excluding +page views. Pause freezes polling, including expanded sessions and history. + +## Scarf company history + +The server uses the existing `ANALYTICS_SCARF_API_TOKEN` bearer token for Scarf's +`/v3/insights/TanStack/aggregations/export` endpoint. Reporting is separate from +the lookup key and requires Scarf reporting access. Exports cover the selected +7, 30, 90, or 365 UTC calendar days, including today. Growth compares website +pixel views with the preceding equal-length window, so today's totals are +incomplete. These are window totals, not lifetime totals. + +Website views include only the site's registered Scarf pixels. Site events +come from the TanStack Site Telemetry package, including browser interactions +and server requests. Events are not added to views. Sources and endpoints are +Scarf's distinct network counts, not people. Company reports join live company +domains exactly, without guessing parent-company relationships. Missing rows or +failed reports show N/A rather than zero. No company data is automatically +unlocked and no Scarf credits are spent on enrichment. + +Successful aggregate exports are cached at the edge for 15 minutes and partial +or unavailable responses for one minute. Live presence still refreshes every +three seconds. Sorting and filtering do not request new exports. Only admins +can request reporting data, and source identifiers and credentials are never +returned to the browser. ## Enable the experiment @@ -46,9 +73,11 @@ contact, not proof that a person is looking at the page. `pageshow` handles restoration from the browser's back/forward cache. Sequence numbers prevent a late heartbeat or departure from overwriting newer tab state. -Each tab retains up to 40 named actions. The admin includes departures from the -last five minutes when requested and returns at most 500 recently seen tabs, -with a visible truncation notice. Sessions disappear from reads at 24 hours and +Each tab retains up to 40 named actions. The company overview aggregates all +eligible tabs without a 500-tab cap. Individual company sessions are fetched +only when expanded, with at most 500 eligible recently seen tabs per company +and a visible truncation notice. The admin includes departures from the last +five minutes when requested. Sessions disappear from reads at 24 hours and are deleted, with their tabs, on the next minute cleanup. ## Company lookup diff --git a/src/components/ProductScarf.tsx b/src/components/ProductScarf.tsx index c37c50416..1f7054551 100644 --- a/src/components/ProductScarf.tsx +++ b/src/components/ProductScarf.tsx @@ -1,18 +1,13 @@ import { useRouterState } from '@tanstack/react-router' import { Scarf } from './Scarf' -const products = [ - { path: '/application-starter', id: 'ed471ec3-aa08-430f-8481-258b3a1f94dc' }, - { path: '/builder', id: 'aa4dcee9-86a2-49b4-9981-40c97cc1f55b' }, - { path: '/stats/npm', id: 'e729f879-e127-48e1-be19-c68f6786c9e8' }, - { path: '/intent/registry', id: '811c34af-38bd-4f06-9762-3f58dea0650a' }, -] +import { productScarfPixels } from '~/utils/analytics/scarf-pixels' export function ProductScarf() { const product = useRouterState({ select: (state) => { const pathname = state.resolvedLocation?.pathname - return products.find( + return productScarfPixels.find( ({ path }) => pathname === path || pathname?.startsWith(`${path}/`), ) }, diff --git a/src/components/admin/CompanyFacetFilter.tsx b/src/components/admin/CompanyFacetFilter.tsx new file mode 100644 index 000000000..78920ade2 --- /dev/null +++ b/src/components/admin/CompanyFacetFilter.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { CaretDownIcon } from '@phosphor-icons/react' + +export function CompanyFacetFilter({ + label, + items, + selected, + onToggle, +}: { + label: string + items: { name: string; value: number }[] + selected: string[] + onToggle: (values: string[]) => void +}) { + const [search, setSearch] = useState('') + const visible = items.filter((item) => + item.name.toLowerCase().includes(search.toLowerCase()), + ) + return ( +
+ + {label} + {selected.length > 0 && ( + {selected.length} + )} + +
+ {label} + setSearch(event.target.value)} + className="mb-1 w-full rounded border border-gray-500/20 bg-transparent px-2 py-1.5 text-xs" + /> + {visible.map((item) => ( + + ))} + {!visible.length && ( +

No matching values.

+ )} +
+
+ ) +} diff --git a/src/components/admin/LiveCompanyCharts.tsx b/src/components/admin/LiveCompanyCharts.tsx index 93bc4de29..b1124d46b 100644 --- a/src/components/admin/LiveCompanyCharts.tsx +++ b/src/components/admin/LiveCompanyCharts.tsx @@ -1,158 +1,64 @@ -import { useMemo } from 'react' -import { areaY, defineChart, lineY } from '@tanstack/charts' -import { Chart } from '@tanstack/charts/react' -import { scaleLinear, scaleUtc } from 'd3' - -export type CompanySample = { time: number; companies: number } - -export function LiveCompanyTrend({ - samples, - count, -}: { - samples: CompanySample[] - count: number -}) { - const definition = useMemo(() => { - const first = samples[0]?.time ?? Date.now() - const last = Math.max(first + 1_000, samples.at(-1)?.time ?? first) - const points = samples.map((sample) => ({ - ...sample, - date: new Date(sample.time), - })) - return defineChart({ - marks: [ - areaY(points, { - x: 'date', - y1: 0, - y2: 'companies', - fill: '#10b981', - fillOpacity: 0.12, - }), - lineY(points, { - x: 'date', - y: 'companies', - stroke: '#10b981', - strokeWidth: 2, - }), - ], - scales: { - x: { - scale: scaleUtc().domain([new Date(first), new Date(last)]), - axis: { - ticks: { - count: 3, - format: (value) => - new Date(value).toLocaleTimeString([], { - minute: '2-digit', - second: '2-digit', - }), - }, - }, - }, - y: { - scale: scaleLinear() - .domain([ - 0, - Math.max(3, ...samples.map((sample) => sample.companies)), - ]) - .nice(), - grid: true, - axis: { - ticks: { - count: 3, - format: (value) => (Number.isInteger(value) ? String(value) : ''), - }, - }, - }, - }, - margin: { top: 10, right: 10, bottom: 24, left: 25 }, - theme: { - background: 'transparent', - foreground: 'currentColor', - muted: '#6b7280', - grid: 'rgba(107, 114, 128, 0.15)', - }, - }) - }, [samples]) - return ( -
-
-

- Identified companies online -

- - Since opened · up to 5m - -
-

- {count} -

-
- {samples.length < 2 ? ( -

- Trend starts with the next live update. -

- ) : ( - - )} -
-
- ) -} - export function CompanyBreakdownChart({ title, items, color, + selected, + onSelect, note, }: { title: string - items: { name: string; value: number }[] + items: { name: string; value: number; values: string[] }[] color: string + selected: string[] + onSelect: (values: string[]) => void note?: string }) { const maximum = Math.max(1, ...items.map((item) => item.value)) return ( -
-
-

+
+
+

{title}

- Companies + Companies
- {items.length === 0 ? ( -

- No identified companies online yet. + {!items.length ? ( +

+ No companies match.

) : ( -
    - {items.map((item) => ( -
  • -
    - - {item.name} - - {item.value} -
    -
    -
    -
    -
  • - ))} +
      + {items.map((item) => { + const active = item.values.every((value) => + selected.includes(value), + ) + return ( +
    • + +
    • + ) + })}
    )}
diff --git a/src/components/admin/LiveCompanySessions.tsx b/src/components/admin/LiveCompanySessions.tsx new file mode 100644 index 000000000..34ed7614a --- /dev/null +++ b/src/components/admin/LiveCompanySessions.tsx @@ -0,0 +1,175 @@ +import { useQuery } from '@tanstack/react-query' +import { CaretRightIcon, CaretDownIcon } from '@phosphor-icons/react' +import { getLiveCompanySessions } from '~/utils/analytics/live.functions' +import { + groupLiveCompanies, + type LiveTab, +} from '~/utils/analytics/live-dashboard' +import { liveActions } from '~/utils/analytics/live' + +const lookupLabels: Record = { + pending: 'Looking up company', + unknown: 'No cached company match', + failed: 'Company lookup failed', + rate_limited: 'Company lookup rate limited', + unconfigured: 'Company lookup not configured', +} +function liveAge(time: Date | string | number, now: string) { + const seconds = Math.max( + 0, + Math.floor((new Date(now).getTime() - new Date(time).getTime()) / 1000), + ) + return seconds < 5 + ? 'Just now' + : seconds < 60 + ? `${seconds}s ago` + : `${Math.floor(seconds / 60)}m ago` +} +export function LiveCompanySessions({ + domain, + paused, +}: { + domain: string + paused: boolean +}) { + const query = useQuery({ + queryKey: ['admin', 'live-company-sessions', domain], + queryFn: () => getLiveCompanySessions({ data: { domain } }), + refetchInterval: paused ? false : 3000, + refetchIntervalInBackground: false, + refetchOnWindowFocus: !paused, + refetchOnReconnect: !paused, + retry: false, + }) + if (query.isPending) + return

Loading sessions...

+ if (query.isError) + return ( +
+

Could not load sessions.

+ +
+ ) + const sessions = groupLiveCompanies(query.data, true).flatMap( + (company) => company.sessions, + ) + return ( +
+ {query.data.truncated && ( +

+ Showing this company's 500 most recently seen tabs. +

+ )} + {sessions.length ? ( + sessions.map((tabs) => ( + + )) + ) : ( +

No recent sessions remain.

+ )} +
+ ) +} + +function SessionDetails({ tabs, now }: { tabs: LiveTab[]; now: string }) { + const first = tabs[0] + const current = + tabs.find((tab) => tab.active && tab.visible) ?? + tabs.find((tab) => tab.active) ?? + first + const active = tabs.some((tab) => tab.active) + const activity = tabs + .flatMap((tab) => tab.activity) + .sort((a, b) => b.at.localeCompare(a.at)) + .slice(0, 40) + return ( +
+ + +
+
+

+ Tabs {tabs.length} +

+
    + {tabs.map((tab) => ( +
  • + + {tab.active + ? tab.visible + ? 'Visible' + : 'Background' + : 'Left'} + + {tab.page} +
  • + ))} +
+

+ Arrived {liveAge(first.arrivedAt, now)} +

+
+
+

Recent activity

+ {activity.length ? ( +
    + {activity.map((event, index) => ( +
  1. + +
    +

    {liveActions[event.action]}

    +

    + {event.page} +

    +
    +
  2. + ))} +
+ ) : ( +

No actions recorded yet.

+ )} +
+
+
+ ) +} diff --git a/src/components/admin/LiveVisitors.tsx b/src/components/admin/LiveVisitors.tsx index f71d0a06b..c521c9679 100644 --- a/src/components/admin/LiveVisitors.tsx +++ b/src/components/admin/LiveVisitors.tsx @@ -1,224 +1,930 @@ -import { Fragment, useEffect, useMemo, useState } from 'react' +import { Fragment, useEffect, useMemo, useRef, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { z } from 'zod' +import { csvFormat } from 'd3' import { createSortedRowModel, + createPaginatedRowModel, rowSortingFeature, + rowPaginationFeature, + rowSelectionFeature, + columnVisibilityFeature, sortFn_alphanumeric, sortFn_basic, tableFeatures, useTable, type ColumnDef, type SortingState, + type PaginationState, + type ColumnVisibilityState, + type RowSelectionState, } from '@tanstack/react-table' import { + ArrowsDownUpIcon, CaretDownIcon, CaretRightIcon, - ArrowsDownUpIcon, MagnifyingGlassIcon, + XIcon, + DownloadSimpleIcon, + ColumnsIcon, } from '@phosphor-icons/react' -import { liveActions } from '~/utils/analytics/live' +import { getScarfCompanyHistory } from '~/utils/analytics/live.functions' +import { historyDaysSchema } from '~/utils/analytics/scarf-history' import { - companyBreakdown, - groupLiveCompanies, - type LiveCompanyRow, - type LiveData, - type LiveTab, + buildCompanyGridRows, + companyFacetCounts, + companyWidgetItems, + filterCompanyRows, + emptyCompanyFilters, + companyGrowth, + csvSafeText, + type CompanyGridRow, + type CompanyFacet, + type LiveOverview, } from '~/utils/analytics/live-dashboard' -import { - CompanyBreakdownChart, - LiveCompanyTrend, - type CompanySample, -} from './LiveCompanyCharts' +import { CompanyBreakdownChart } from './LiveCompanyCharts' +import { CompanyFacetFilter } from './CompanyFacetFilter' +import { LiveCompanySessions } from './LiveCompanySessions' const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), + rowSelectionFeature, + columnVisibilityFeature, }) -const initialSorting: SortingState = [{ id: 'name', desc: false }] -const columns: ColumnDef[] = [ - { accessorKey: 'name', header: 'Company', sortFn: sortFn_alphanumeric }, +const initialSorting: SortingState = [ + { id: 'unmatched', desc: false }, + { id: 'siteViews', desc: true }, + { id: 'actions', desc: true }, + { id: 'sessions', desc: true }, +] +const initialPagination: PaginationState = { pageIndex: 0, pageSize: 50 } +const initialVisibility: ColumnVisibilityState = { + unmatched: false, + tabs: false, + recentViews: false, + siteEvents: false, + origins: false, + endpoints: false, + lastScarfSeen: false, +} +const initialSelection: RowSelectionState = {} +const visibilitySchema = z.record(z.string(), z.boolean()) +const facets: CompanyFacet[] = ['library', 'size', 'industry', 'stage'] +const widgetFacets: CompanyFacet[] = ['library', 'size', 'industry'] +const facetLabels: Record = { + library: 'Library', + size: 'Company size', + industry: 'Industry', + stage: 'Funnel stage', +} +const number = new Intl.NumberFormat('en-US') +function numeric(value: number | undefined) { + return value === undefined ? ( + + N/A + + ) : ( + number.format(value) + ) +} +function age(value: Date, now: string) { + const seconds = Math.max( + 0, + Math.floor((new Date(now).getTime() - new Date(value).getTime()) / 1000), + ) + return seconds < 5 + ? 'Now' + : seconds < 60 + ? `${seconds}s` + : `${Math.floor(seconds / 60)}m` +} +function growthLabel(row: CompanyGridRow) { + const growth = companyGrowth(row) + if (row.history?.previousViews === 0 && (row.history.views ?? 0) > 0) + return 'New' + return growth === undefined + ? 'N/A' + : `${growth > 0 ? '+' : ''}${growth.toFixed(0)}%` +} +const columns: ColumnDef[] = [ + { + id: 'unmatched', + accessorFn: (row) => (row.id ? 0 : 1), + sortFn: sortFn_basic, + enableHiding: false, + }, + { id: 'select', header: 'Select', enableSorting: false, enableHiding: false }, + { + id: 'name', + accessorKey: 'name', + header: 'Company', + sortFn: sortFn_alphanumeric, + enableHiding: false, + }, { id: 'industry', - accessorFn: (row) => row.company?.industry ?? '', + accessorKey: 'industry', header: 'Industry', sortFn: sortFn_alphanumeric, + cell: ({ row }) => ( + + {row.original.industry} + + ), }, { id: 'size', - accessorFn: (row) => row.company?.company_size ?? '', - header: 'Company size', - enableSorting: false, + accessorFn: (row) => row.employeeMin, + header: 'Employees', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => row.original.company?.company_size || 'N/A', }, { id: 'libraries', accessorFn: (row) => row.libraries.join(', '), - header: 'On site', - enableSorting: false, + header: 'Libraries', + sortFn: sortFn_alphanumeric, + cell: ({ row }) => ( + + {row.original.libraries.join(', ') || 'None active'} + + ), + }, + { + id: 'sessions', + accessorKey: 'activeSessions', + header: 'Sessions', + sortFn: sortFn_basic, + cell: ({ row }) => numeric(row.original.activeSessions), + }, + { + id: 'tabs', + accessorKey: 'activeTabs', + header: 'Tabs', + sortFn: sortFn_basic, + cell: ({ row }) => numeric(row.original.activeTabs), + }, + { + id: 'actions', + accessorKey: 'actions', + header: 'Actions · 5m', + sortFn: sortFn_basic, + cell: ({ row }) => numeric(row.original.actions), + }, + { + id: 'recentViews', + accessorKey: 'recentViews', + header: 'Views · 5m', + sortFn: sortFn_basic, + cell: ({ row }) => numeric(row.original.recentViews), + }, + { + id: 'siteViews', + accessorFn: (row) => row.history?.views, + header: 'Site views', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => numeric(row.original.history?.views), + }, + { + id: 'growth', + accessorFn: (row) => companyGrowth(row), + header: 'Change', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => ( + 0 + ? 'text-emerald-600 dark:text-emerald-400' + : (companyGrowth(row.original) ?? 0) < 0 + ? 'text-amber-600 dark:text-amber-400' + : 'text-gray-500' + } + > + {growthLabel(row.original)} + + ), + }, + { + id: 'siteEvents', + accessorFn: (row) => row.history?.siteEvents, + header: 'Site events', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => numeric(row.original.history?.siteEvents), }, { - id: 'pages', - accessorFn: (row) => row.pages.length, - header: 'Pages now', + id: 'origins', + accessorFn: (row) => row.history?.origins, + header: 'Sources', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => numeric(row.original.history?.origins), + }, + { + id: 'endpoints', + accessorFn: (row) => row.history?.endpoints, + header: 'Endpoints', + sortFn: sortFn_basic, + sortUndefined: 'last', + cell: ({ row }) => numeric(row.original.history?.endpoints), + }, + { + id: 'stage', + accessorFn: (row) => row.history?.stage, + header: 'Funnel stage', + sortFn: sortFn_alphanumeric, + sortUndefined: 'last', + cell: ({ row }) => ( + + {row.original.history?.stage?.replaceAll('-', ' ') || 'N/A'} + + ), + }, + { + id: 'lastScarfSeen', + accessorFn: (row) => row.history?.lastSeen, + header: 'Scarf last seen', + sortFn: sortFn_alphanumeric, + sortUndefined: 'last', + cell: ({ row }) => + row.original.history?.lastSeen + ? new Date(row.original.history.lastSeen).toLocaleDateString() + : 'N/A', + }, + { + id: 'lastSeen', + accessorFn: (row) => new Date(row.lastSeen).getTime(), + header: 'Seen', sortFn: sortFn_basic, }, - { accessorKey: 'actions', header: 'Actions · 5m', sortFn: sortFn_basic }, - { accessorKey: 'lastSeen', header: 'Last seen', sortFn: sortFn_basic }, ] -const lookupLabels: Record = { - pending: 'Looking up company', - unknown: 'No cached company match', - failed: 'Company lookup failed', - rate_limited: 'Company lookup rate limited', - unconfigured: 'Company lookup not configured', -} -function age(time: Date | string | number, now: string) { - const seconds = Math.max( - 0, - Math.floor((new Date(now).getTime() - new Date(time).getTime()) / 1000), - ) - return seconds < 5 - ? 'Just now' - : seconds < 60 - ? `${seconds}s ago` - : `${Math.floor(seconds / 60)}m ago` +const columnHelp: Record = { + sessions: 'Anonymous sessions with a heartbeat in the last 65 seconds.', + actions: + 'Recorded interactions from the last five minutes, limited to 40 retained events per tab.', + recentViews: + 'Recorded browser page-view events in the last five minutes, from retained tab activity.', + siteViews: + 'Scarf pixel views for the pixels embedded in tanstack.com, in the selected history window. Not unique people.', + growth: + 'Pixel-view change against the previous equal-length window. N/A means a comparison was not reported.', + siteEvents: + 'Events from the dedicated TanStack Site Telemetry package. Includes page views, actions, and server requests. These are not added to pixel views.', + origins: + 'Scarf unique origins across the selected site pixels. These are sources, not identified people.', + endpoints: + 'Scarf unique endpoints across the selected site pixels. These are not individual identities.', } -export function LiveVisitors({ data }: { data: LiveData }) { +export function LiveVisitors({ + data, + paused = false, +}: { + data: LiveOverview + paused?: boolean +}) { const [expanded, setExpanded] = useState(new Set()) - const [showRecent, setShowRecent] = useState(false) - const [search, setSearch] = useState('') + const [filters, setFilters] = useState(emptyCompanyFilters) const [sorting, setSorting] = useState(initialSorting) - const initialSamples: CompanySample[] = [] - const [samples, setSamples] = useState(initialSamples) - const activeCompanies = useMemo(() => groupLiveCompanies(data, false), [data]) - const count = activeCompanies.filter((company) => company.id).length + const [pagination, setPagination] = useState(initialPagination) + const [visibility, setVisibility] = useState(initialVisibility) + const [selection, setSelection] = useState(initialSelection) + const [selectedOnly, setSelectedOnly] = useState(false) + const [comfortable, setComfortable] = useState(false) + const [showBreakdowns, setShowBreakdowns] = useState(true) + const [days, setDays] = useState(historyDaysSchema.parse(30)) + const [preferencesLoaded, setPreferencesLoaded] = useState(false) + const searchInput: { current: HTMLInputElement | null } = useRef(null) + const history = useQuery({ + queryKey: ['admin', 'scarf-company-history', days], + queryFn: () => getScarfCompanyHistory({ data: { days } }), + staleTime: 15 * 60_000, + refetchInterval: paused ? false : 15 * 60_000, + refetchOnWindowFocus: false, + refetchOnReconnect: !paused, + retry: false, + }) useEffect(() => { - const time = new Date(data.now).getTime() - setSamples((previous) => - previous.at(-1)?.time === time - ? previous - : [ - ...previous.filter((sample) => sample.time >= time - 5 * 60_000), - { time, companies: count }, - ], - ) - }, [data.now, count]) - const companies = useMemo( + try { + const saved = localStorage.getItem('tanstack.live-companies.columns.v1') + if (saved) { + const parsed = visibilitySchema.safeParse(JSON.parse(saved)) + if (parsed.success) + setVisibility({ + ...initialVisibility, + ...parsed.data, + unmatched: false, + }) + } + } catch {} + setPreferencesLoaded(true) + }, []) + useEffect(() => { + if (preferencesLoaded) + try { + localStorage.setItem( + 'tanstack.live-companies.columns.v1', + JSON.stringify(visibility), + ) + } catch {} + }, [visibility, preferencesLoaded]) + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + const target = event.target + if ( + event.key === '/' && + !event.metaKey && + !event.ctrlKey && + !( + target instanceof HTMLElement && + (target.matches('input,textarea,select') || target.isContentEditable) + ) + ) { + event.preventDefault() + searchInput.current?.focus() + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, []) + const allRows = useMemo( + () => buildCompanyGridRows(data, history.data), + [data, history.data], + ) + const filtered = useMemo( + () => filterCompanyRows(allRows, filters), + [allRows, filters], + ) + const gridRows = useMemo( () => - groupLiveCompanies(data, showRecent).filter((row) => - `${row.name} ${row.id} ${row.company?.industry ?? ''} ${row.libraries.join(' ')} ${row.sessions - .flat() - .map((tab) => tab.page) - .join(' ')}` - .toLowerCase() - .includes(search.trim().toLowerCase()), - ), - [data, showRecent, search], + selectedOnly + ? filtered.filter((row) => selection[row.id || 'unmatched']) + : filtered, + [filtered, selectedOnly, selection], ) + const activeRows = allRows.filter((row) => row.active) + const activeCount = activeRows.filter((row) => row.id).length + const summaryRows = gridRows.filter((row) => row.active) + const selectedRows = allRows.filter((row) => selection[row.id || 'unmatched']) const table = useTable({ features, columns, - data: companies, + data: gridRows, getRowId: (row) => row.id || 'unmatched', - state: { sorting }, - onSortingChange: setSorting, + state: { + sorting, + pagination, + columnVisibility: visibility, + rowSelection: selection, + }, + onSortingChange: (updater) => + setSorting((previous) => [ + { id: 'unmatched', desc: false }, + ...(typeof updater === 'function' ? updater(previous) : updater).filter( + (sort) => sort.id !== 'unmatched', + ), + ]), + onPaginationChange: setPagination, + onColumnVisibilityChange: setVisibility, + onRowSelectionChange: setSelection, + autoResetPageIndex: false, }) - const sorted = table.getRowModel().rows - const rows = [ - ...sorted.filter((row) => row.original.id), - ...sorted.filter((row) => !row.original.id), - ] - const toggle = (id: string) => + useEffect(() => { + const lastPage = Math.max( + 0, + Math.ceil(gridRows.length / pagination.pageSize) - 1, + ) + if (pagination.pageIndex > lastPage) + setPagination((previous) => ({ ...previous, pageIndex: lastPage })) + }, [gridRows.length, pagination.pageSize, pagination.pageIndex]) + useEffect(() => { + setPagination((previous) => + previous.pageIndex === 0 ? previous : { ...previous, pageIndex: 0 }, + ) + }, [filters]) + const toggleFacet = (facet: CompanyFacet, values: string[]) => { + setFilters((previous) => ({ + ...previous, + [facet]: values.every((value) => previous[facet].includes(value)) + ? previous[facet].filter((value) => !values.includes(value)) + : [...new Set([...previous[facet], ...values])], + })) + setPagination((previous) => ({ ...previous, pageIndex: 0 })) + } + const toggleCompany = (id: string) => setExpanded((previous) => { const next = new Set(previous) if (next.has(id)) next.delete(id) else next.add(id) return next }) + const filterCount = + facets.reduce((count, facet) => count + filters[facet].length, 0) + + Number(Boolean(filters.search)) + + Number(Boolean(filters.minViews)) + + Number(Boolean(filters.minActions)) + + Number(filters.identifiedOnly) + + Number(filters.showRecent) + const exportRows = () => { + const records = ( + selectedRows.length + ? selectedRows + : table.getPrePaginatedRowModel().rows.map((row) => row.original) + ).map((row) => ({ + Company: csvSafeText(row.name), + Domain: csvSafeText(row.id), + Industry: csvSafeText(row.industry), + Employees: csvSafeText(row.company?.company_size || ''), + Libraries: csvSafeText(row.libraries.join(', ')), + Sessions: row.activeSessions, + Actions5m: row.actions, + RecentViews5m: row.recentViews, + SitePixelViews: row.history?.views ?? '', + PreviousPixelViews: row.history?.previousViews ?? '', + Change: csvSafeText(growthLabel(row)), + SiteEvents: row.history?.siteEvents ?? '', + Sources: row.history?.origins ?? '', + Endpoints: row.history?.endpoints ?? '', + FunnelStage: csvSafeText(row.history?.stage || ''), + HistoryStart: history.data?.start ?? '', + HistoryEndExclusive: history.data?.end ?? '', + LastSeen: new Date(row.lastSeen).toISOString(), + })) + const url = URL.createObjectURL( + new Blob([csvFormat(records)], { type: 'text/csv;charset=utf-8' }), + ) + const link = document.createElement('a') + link.href = url + link.download = `live-companies-${new Date().toISOString().slice(0, 10)}.csv` + link.click() + URL.revokeObjectURL(url) + } + const rows = table.getRowModel().rows return ( -
+
{!data.enabled && ( -

- Visitor collection is off. +

+ Collection is off.

)} {!data.lookupConfigured && ( -

+

Company matching needs the Scarf lookup key.

)} - {data.truncated && ( -

- Overview and grid cover the 500 most recently seen tabs. -

+
+
+ + + {summaryRows.filter((row) => row.id).length} + {' '} + + companies online + + + + + {number.format( + summaryRows.reduce( + (total, row) => total + row.activeSessions, + 0, + ), + )} + {' '} + sessions + + + + {number.format( + summaryRows.reduce((total, row) => total + row.actions, 0), + )} + {' '} + actions · 5m + +
+
+ + +
+
+ {showBreakdowns && ( +
+ {widgetFacets.map((facet) => ( + toggleFacet(facet, values)} + note={ + facet === 'size' + ? "Grouped by the lower bound of Scarf's reported employee range." + : facet === 'library' + ? 'A company can appear in multiple libraries. Other combines the remaining category counts.' + : undefined + } + /> + ))} +
)} -
- - - +
+ {history.isFetching ? ( + Loading Scarf history... + ) : history.isError ? ( + <> + + Scarf history could not load. + + + + ) : history.data?.status === 'unconfigured' ? ( + + Scarf reporting token is not configured. + + ) : history.data?.status === 'unavailable' ? ( + <> + + Scarf history unavailable. Live activity is still updating. + + + + ) : ( + history.data && ( + <> + + Scarf synced{' '} + {new Date(history.data.fetchedAt).toLocaleTimeString()} + + {history.data.status === 'partial' && ( + + Some metrics unavailable + + )} + + Pixel views and site events are separate measurements. + + + ) + )}
-
-

- Companies on site{' '} - - {companies.filter((row) => row.id).length} - -

-
- -
-
+
+
+
+ {facets.map((facet) => ( + toggleFacet(facet, values)} + /> + ))} + + +
+ +
+ + +
+ Visible columns + {table + .getAllLeafColumns() + .filter((column) => column.getCanHide()) + .map((column) => ( + + ))} +
+
+ +
-
+
+ + {filtered.filter((row) => row.id).length} /{' '} + { + allRows.filter( + (row) => row.id && (filters.showRecent || row.active), + ).length + }{' '} + companies + + + + {(selectedRows.length > 0 || selectedOnly) && ( + <> + + + + )} + {filterCount > 0 && ( + + )} +
+ {facets.some((facet) => filters[facet].length > 0) && ( +
+ {facets.flatMap((facet) => + filters[facet].map((value) => ( + + )), + )} +
+ )} +
- + {table.getHeaderGroups().map((group) => ( {group.headers.map((header) => { const sort = header.column.getIsSorted() + const index = sorting.findIndex( + (item) => item.id === header.column.id, + ) return ( - - - - - - - + + ))} {open && ( )} ) })} - {rows.length === 0 && ( + {!rows.length && ( @@ -380,137 +1118,54 @@ export function LiveVisitors({ data }: { data: LiveData }) {
- {header.column.getCanSort() ? ( + {header.column.id === 'select' ? ( + { + if (element) + element.indeterminate = + table.getIsSomePageRowsSelected() + }} + onChange={(event) => + table.toggleAllPageRowsSelected( + event.target.checked, + ) + } + className="accent-emerald-600" + /> + ) : header.column.getCanSort() ? ( @@ -263,116 +990,127 @@ export function LiveVisitors({ data }: { data: LiveData }) { return (
- - {open ? ( - - - {company.company?.industry || 'Unknown'} - - - {company.company?.company_size || 'Unknown'} - -
- {company.libraries.slice(0, 2).map((library) => ( - - {library} - - ))} - {company.libraries.length > 2 && ( + + ) : cell.column.id === 'lastSeen' ? ( - +{company.libraries.length - 2} + {age(company.lastSeen, data.now)} + ) : ( + )} -
-
- {company.pages.length} - - {company.actions} - -
- +
+
+ {company.name} + {company.id && ( + + {company.id} + + )} + + {company.activeSessions} active sessions ·{' '} + {company.activeTabs} tabs + + {company.company?.linkedin_url && + /^https:\/\/(www\.)?linkedin\.com\//.test( + company.company.linkedin_url, + ) && ( + + LinkedIn + + )} +
+ +
- {search - ? 'No companies or pages match your search.' + {filterCount || selectedOnly + ? 'No companies match these filters.' : 'No companies on public pages right now.'}
-
- - Company matches describe the network, not a verified employer. - - Missing heartbeats expire after 65 seconds. +
+ Network matches, not verified employers. +
+ + + + {Math.min( + pagination.pageIndex + 1, + Math.max(1, table.getPageCount()), + )}{' '} + / {Math.max(1, table.getPageCount())} + + +
) } - -function CompanySessions({ - company, - now, -}: { - company: LiveCompanyRow - now: string -}) { - return ( -
-
-

Sessions at {company.name}

- - { - company.sessions.filter((tabs) => tabs.some((tab) => tab.active)) - .length - }{' '} - active · {company.sessions.length} in view - -
-
- {company.sessions.map((tabs) => ( - - ))} -
-
- ) -} - -function SessionDetails({ tabs, now }: { tabs: LiveTab[]; now: string }) { - const first = tabs[0] - const current = - tabs.find((tab) => tab.active && tab.visible) ?? - tabs.find((tab) => tab.active) ?? - first - const active = tabs.some((tab) => tab.active) - const activity = tabs - .flatMap((tab) => tab.activity) - .sort((a, b) => b.at.localeCompare(a.at)) - .slice(0, 40) - return ( -
- - -
-
-

- Tabs {tabs.length} -

-
    - {tabs.map((tab) => ( -
  • - - {tab.active - ? tab.visible - ? 'Visible' - : 'Background' - : 'Left'} - - {tab.page} -
  • - ))} -
-

- Arrived {age(first.arrivedAt, now)} -

-
-
-

Recent activity

- {activity.length ? ( -
    - {activity.map((event, index) => ( -
  1. - -
    -

    {liveActions[event.action]}

    -

    - {event.page} -

    -
    -
  2. - ))} -
- ) : ( -

No actions recorded yet.

- )} -
-
-
- ) -} diff --git a/src/routes/admin/live.tsx b/src/routes/admin/live.tsx index 191bb4626..116334d6e 100644 --- a/src/routes/admin/live.tsx +++ b/src/routes/admin/live.tsx @@ -1,53 +1,83 @@ +import { useState } from 'react' import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { BuildingsIcon } from '@phosphor-icons/react' -import { useAdminGuard } from '~/hooks/useAdminGuard' import { - AdminAccessDenied, - AdminLoading, - AdminPageHeader, -} from '~/components/admin' + BuildingsIcon, + PauseIcon, + PlayIcon, + ArrowClockwiseIcon, +} from '@phosphor-icons/react' +import { useAdminGuard } from '~/hooks/useAdminGuard' +import { AdminAccessDenied, AdminLoading } from '~/components/admin' import { LiveVisitors } from '~/components/admin/LiveVisitors' -import { getLiveVisitors } from '~/utils/analytics/live.functions' +import { getLiveCompanyOverview } from '~/utils/analytics/live.functions' export const Route = createFileRoute('/admin/live')({ component: LiveVisitorsPage, }) - function LiveVisitorsPage() { const guard = useAdminGuard() + const [paused, setPaused] = useState(false) const query = useQuery({ - queryKey: ['admin', 'live-visitors'], - queryFn: () => getLiveVisitors(), + queryKey: ['admin', 'live-company-overview'], + queryFn: () => getLiveCompanyOverview(), enabled: guard.status === 'authorized', - refetchInterval: 3000, + refetchInterval: paused ? false : 3000, refetchIntervalInBackground: false, + refetchOnWindowFocus: !paused, + refetchOnReconnect: !paused, retry: false, }) if (guard.status === 'loading') return if (guard.status === 'denied') return return ( -
- } - title="Live companies" - actions={ - +
+
+

+ + Live companies +

+
+ {query.isError ? 'Updates interrupted' - : query.data - ? `Updated ${new Date(query.data.now).toLocaleTimeString()}` - : 'Connecting'} + : paused + ? 'Paused' + : query.data + ? `Updated ${new Date(query.data.now).toLocaleTimeString()}` + : 'Connecting'} - } - /> + + +
+
{query.isPending && } {query.isError && (
-

Couldn’t load live visitors. The last view may be out of date.

+

Could not refresh companies. The last view may be out of date.

)} - {query.data && } + {query.data && }
) } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 04c764b89..c97e1666d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,3 +1,4 @@ +import { homepageScarfPixel } from '~/utils/analytics/scarf-pixels' import * as React from 'react' import { Link, createFileRoute } from '@tanstack/react-router' @@ -74,7 +75,7 @@ function Index() { return ( <> - +
{/* Hero — Figma node 802:2027. Full-bleed palm/gradient photo card: diff --git a/src/server/runtime/blob-storage.server.ts b/src/server/runtime/blob-storage.server.ts index ab4f4a7af..564da32b3 100644 --- a/src/server/runtime/blob-storage.server.ts +++ b/src/server/runtime/blob-storage.server.ts @@ -189,7 +189,7 @@ function getCacheRequest(name: BlobStorageName, key: string) { return new Request(url) } -function getDefaultRuntimeCache() { +export function getDefaultRuntimeCache() { if (!isIsolateRuntime() || typeof caches === 'undefined') { return undefined } diff --git a/src/utils/analytics/live-dashboard.ts b/src/utils/analytics/live-dashboard.ts index 0564407a5..79579bf8f 100644 --- a/src/utils/analytics/live-dashboard.ts +++ b/src/utils/analytics/live-dashboard.ts @@ -109,3 +109,167 @@ export function companyBreakdown( }, ] } + +export type LiveOverview = Awaited< + ReturnType +> +export type CompanyFacet = 'library' | 'industry' | 'size' | 'stage' +export type CompanyFilters = { + search: string + library: string[] + industry: string[] + size: string[] + stage: string[] + minViews: string + minActions: string + showRecent: boolean + identifiedOnly: boolean +} +export const emptyCompanyFilters: CompanyFilters = { + search: '', + library: [], + industry: [], + size: [], + stage: [], + minViews: '', + minActions: '', + showRecent: false, + identifiedOnly: false, +} + +export function employeeLowerBound(value: string | null | undefined) { + const match = value?.match(/(\d[\d,]*(?:\.\d+)?)\s*([km])?/i) + if (!match) return undefined + return ( + Number(match[1].replaceAll(',', '')) * + (match[2]?.toLowerCase() === 'm' + ? 1_000_000 + : match[2]?.toLowerCase() === 'k' + ? 1000 + : 1) + ) +} +export function companySizeBucket(value: string | null | undefined) { + const count = employeeLowerBound(value) + return count === undefined + ? 'Unspecified' + : count < 250 + ? '1-249' + : count < 1000 + ? '250-999' + : count < 10_000 + ? '1K-9,999' + : '10K+' +} +export function buildCompanyGridRows( + data: LiveOverview, + history: import('./scarf-history').ScarfHistory | undefined, +) { + const reports = new Map( + history?.companies.map((company) => [company.domain, company]), + ) + return data.companies.map((company) => { + const report = reports.get(company.id) + return { + ...company, + name: + company.company?.name || + report?.name || + company.id || + 'Unmatched traffic', + libraries: [...new Set(company.pages.map(pageLibrary))], + active: company.activeSessions > 0, + industry: company.company?.industry || 'Unspecified', + sizeBucket: companySizeBucket(company.company?.company_size), + employeeMin: employeeLowerBound(company.company?.company_size), + history: report, + } + }) +} +export type CompanyGridRow = ReturnType[number] +export function companyFacetValues(row: CompanyGridRow, facet: CompanyFacet) { + return facet === 'library' + ? row.libraries + : facet === 'industry' + ? [row.industry] + : facet === 'size' + ? [row.sizeBucket] + : [row.history?.stage || 'Unspecified'] +} +export function filterCompanyRows( + rows: CompanyGridRow[], + filters: CompanyFilters, + omit?: CompanyFacet, +) { + const facets: CompanyFacet[] = ['library', 'industry', 'size', 'stage'] + const search = filters.search.trim().toLowerCase() + return rows.filter( + (row) => + (filters.showRecent || row.active) && + (!filters.identifiedOnly || Boolean(row.id)) && + (!search || + `${row.name} ${row.id} ${row.industry} ${row.libraries.join(' ')} ${row.pages.join(' ')}` + .toLowerCase() + .includes(search)) && + (!filters.minViews || + (row.history?.views !== undefined && + row.history.views >= Number(filters.minViews))) && + (!filters.minActions || row.actions >= Number(filters.minActions)) && + facets.every( + (facet) => + omit === facet || + !filters[facet].length || + filters[facet].some((value) => + companyFacetValues(row, facet).includes(value), + ), + ), + ) +} +export function companyFacetCounts( + rows: CompanyGridRow[], + facet: CompanyFacet, + activeOnly = true, +) { + const counts = new Map() + for (const row of rows) { + if (!row.id || (activeOnly && !row.active)) continue + for (const value of companyFacetValues(row, facet)) + counts.set(value, (counts.get(value) ?? 0) + 1) + } + return [...counts] + .map(([name, value]) => ({ name, value, values: [name] })) + .sort((a, b) => + facet === 'size' + ? ['1-249', '250-999', '1K-9,999', '10K+', 'Unspecified'].indexOf( + a.name, + ) - + ['1-249', '250-999', '1K-9,999', '10K+', 'Unspecified'].indexOf( + b.name, + ) + : b.value - a.value || a.name.localeCompare(b.name), + ) +} +export function companyWidgetItems( + items: ReturnType, +) { + return items.length <= 6 + ? items + : [ + ...items.slice(0, 5), + { + name: 'Other', + value: items.slice(5).reduce((count, item) => count + item.value, 0), + values: items.slice(5).flatMap((item) => item.values), + }, + ] +} +export function companyGrowth(row: CompanyGridRow) { + const current = row.history?.views + const previous = row.history?.previousViews + return current === undefined || previous === undefined || previous === 0 + ? undefined + : ((current - previous) / previous) * 100 +} +export function csvSafeText(value: string) { + return /^\s*[=+\-@]|^[\t\r\n]/.test(value) ? `'${value}` : value +} diff --git a/src/utils/analytics/live.functions.ts b/src/utils/analytics/live.functions.ts index a3d95eba7..b4ec11e01 100644 --- a/src/utils/analytics/live.functions.ts +++ b/src/utils/analytics/live.functions.ts @@ -1,5 +1,9 @@ +import { z } from 'zod' +import { historyDaysSchema } from './scarf-history' +import { loadScarfHistory } from './scarf-history.server' +import { requireAdmin } from '~/utils/roles.server' import { createServerFn } from '@tanstack/react-start' -import { listLiveVisitors } from './live.server' +import { listLiveVisitors, listLiveCompanyOverview } from './live.server' import { getLiveConfig } from './scarf-lookup.server' export const getLiveVisitors = createServerFn({ method: 'GET' }).handler( @@ -9,3 +13,23 @@ export const getLiveVisitors = createServerFn({ method: 'GET' }).handler( export const getLiveVisitorsEnabled = createServerFn({ method: 'GET' }).handler( async () => (await getLiveConfig()).enabled, ) + +export const getLiveCompanyOverview = createServerFn({ method: 'GET' }).handler( + async () => listLiveCompanyOverview(), +) +export const getLiveCompanySessions = createServerFn({ method: 'GET' }) + .validator( + z.object({ + domain: z + .string() + .max(253) + .regex(/^(?:[a-z0-9.-]+)?$/), + }), + ) + .handler(async ({ data }) => listLiveVisitors(data.domain)) +export const getScarfCompanyHistory = createServerFn({ method: 'GET' }) + .validator(z.object({ days: historyDaysSchema })) + .handler(async ({ data }) => { + await requireAdmin() + return loadScarfHistory(data.days) + }) diff --git a/src/utils/analytics/live.server.ts b/src/utils/analytics/live.server.ts index 0bd95c553..879b93999 100644 --- a/src/utils/analytics/live.server.ts +++ b/src/utils/analytics/live.server.ts @@ -1,3 +1,4 @@ +import { z } from 'zod' import { and, desc, eq, gt, lt, sql } from 'drizzle-orm' import { isIP } from 'node:net' import { db } from '~/db/client' @@ -10,6 +11,7 @@ import { import { getLiveConfig, lookupScarfCompany } from './scarf-lookup.server' import { isLivePage, + liveCompanySchema, livePublicPagePattern, liveForbiddenPagePattern, liveEventSchema, @@ -172,7 +174,7 @@ export async function recordLiveEvent( } } -export async function listLiveVisitors() { +export async function listLiveVisitors(domain?: string) { await requireAdmin() const config = await getLiveConfig() const now = Date.now() @@ -203,6 +205,9 @@ export async function listLiveVisitors() { and( gt(liveTabs.lastSeen, new Date(now - 5 * 60_000)), gt(liveSessions.createdAt, new Date(now - LIVE_RETENTION_MS)), + domain === undefined + ? undefined + : sql`coalesce(lower(${liveSessions.company}->>'domain'), '') = ${domain}`, sql`length(${liveTabs.page}) <= 400`, sql`${liveTabs.page} ~ ${livePublicPagePattern.source}`, sql`${liveTabs.page} !~ ${liveForbiddenPagePattern.source}`, @@ -234,3 +239,69 @@ export async function pruneLiveVisitors(now = Date.now()) { .delete(liveSessions) .where(lt(liveSessions.createdAt, new Date(now - LIVE_RETENTION_MS))) } + +export const liveCompanyOverviewRowSchema = z.object({ + id: z.string(), + company: liveCompanySchema.nullable(), + activeSessions: z.coerce.number().int().nonnegative(), + activeTabs: z.coerce.number().int().nonnegative(), + visibleTabs: z.coerce.number().int().nonnegative(), + recentSessions: z.coerce.number().int().nonnegative(), + pages: z.array(z.string()), + actions: z.coerce.number().int().nonnegative(), + recentViews: z.coerce.number().int().nonnegative(), + lastSeen: z.coerce.date(), +}) + +export function liveCompanyOverviewSql(now: number) { + const activeSince = new Date(now - LIVE_TIMEOUT_MS).toISOString() + const recentSince = new Date(now - 5 * 60_000).toISOString() + const retainedSince = new Date(now - LIVE_RETENTION_MS).toISOString() + return sql` + WITH eligible AS ( + SELECT s.id AS session_id, s.company, t.page, t.visible, t.last_seen, t.activity, + (t.left_at IS NULL AND t.last_seen > ${activeSince}::timestamptz) AS active + FROM live_tabs t JOIN live_sessions s ON s.id = t.session_id + WHERE t.last_seen > ${recentSince}::timestamptz AND s.created_at > ${retainedSince}::timestamptz + AND length(t.page) <= 400 AND t.page ~ ${livePublicPagePattern.source} + AND t.page !~ ${liveForbiddenPagePattern.source} + ) + SELECT coalesce(lower(e.company->>'domain'), '') AS id, + max(e.company::text)::jsonb AS company, + count(DISTINCT e.session_id) FILTER (WHERE e.active) AS "activeSessions", + count(*) FILTER (WHERE e.active) AS "activeTabs", + count(*) FILTER (WHERE e.active AND e.visible) AS "visibleTabs", + count(DISTINCT e.session_id) AS "recentSessions", + coalesce(jsonb_agg(DISTINCT e.page ORDER BY e.page) FILTER (WHERE e.active), '[]'::jsonb) AS pages, + coalesce(sum(a.actions), 0) AS actions, coalesce(sum(a.views), 0) AS "recentViews", + max(e.last_seen) AS "lastSeen" + FROM eligible e + CROSS JOIN LATERAL ( + SELECT count(*) FILTER (WHERE event->>'action' <> 'page_view') AS actions, + count(*) FILTER (WHERE event->>'action' = 'page_view') AS views + FROM jsonb_array_elements(e.activity) event + WHERE event->>'at' >= ${recentSince} + AND length(event->>'page') <= 400 AND event->>'page' ~ ${livePublicPagePattern.source} + AND event->>'page' !~ ${liveForbiddenPagePattern.source} + ) a + GROUP BY coalesce(lower(e.company->>'domain'), '') + ORDER BY id + ` +} + +export async function listLiveCompanyOverview() { + await requireAdmin() + const config = await getLiveConfig() + const now = Date.now() + const companies = config.enabled + ? z + .array(liveCompanyOverviewRowSchema) + .parse(await db.execute(liveCompanyOverviewSql(now))) + : [] + return { + enabled: config.enabled, + lookupConfigured: Boolean(config.key), + now: new Date(now).toISOString(), + companies, + } +} diff --git a/src/utils/analytics/scarf-history.server.ts b/src/utils/analytics/scarf-history.server.ts new file mode 100644 index 000000000..3d18d9919 --- /dev/null +++ b/src/utils/analytics/scarf-history.server.ts @@ -0,0 +1,191 @@ +import { libraries } from '~/libraries/libraries' +import { getHostRuntimeEnv } from '~/server/runtime/host.server' +import { getDefaultRuntimeCache } from '~/server/runtime/blob-storage.server' +import { homepageScarfPixel, productScarfPixels } from './scarf-pixels' +import { + companyHistoryFromReports, + historyWindow, + scarfAggregationRowSchema, + type ScarfAggregationRow, + scarfHistorySchema, + type ScarfHistory, +} from './scarf-history' + +// This is the event-only /site-events route verified in Scarf's package settings. +const siteTelemetryPackage = 'b942b596-f90d-4072-a763-86fb5fbe5852' +const sitePixels = [ + ...new Set([ + homepageScarfPixel, + ...productScarfPixels.map((pixel) => pixel.id), + ...libraries.flatMap((library) => + library.scarfId ? [library.scarfId] : [], + ), + ]), +] + +export function scarfReportUrl( + start: string, + end: string, + source: 'views' | 'events', +) { + const url = new URL( + 'https://api.scarf.sh/v3/insights/TanStack/aggregations/export', + ) + for (const [key, value] of Object.entries({ + start_date: start, + end_date: end, + rollup: 'total', + breakdown: 'by-company', + format: 'ndjson', + group_by_artifact: 'false', + group_by_artifact_type: 'false', + include_low_confidence: 'false', + })) + url.searchParams.set(key, value) + url.searchParams.set( + 'package_id', + source === 'events' ? siteTelemetryPackage : 'none', + ) + if (source === 'views') + for (const id of sitePixels) + url.searchParams.append('tracking_pixel_id', id) + else url.searchParams.set('tracking_pixel_id', 'none') + return url +} + +class ScarfReportError extends Error { + constructor(readonly detail: string) { + super(detail) + } +} + +async function report( + start: string, + end: string, + source: 'views' | 'events', + token: string, +) { + const response = await fetch(scarfReportUrl(start, end, source), { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/x-ndjson', + }, + signal: AbortSignal.timeout(30_000), + }) + if (!response.ok) { + await response.body?.cancel() + throw new ScarfReportError(`HTTP ${response.status}`) + } + const rows: ScarfAggregationRow[] = [] + const reader = response.body?.getReader() + if (!reader) throw new ScarfReportError('empty response') + const decoder = new TextDecoder() + let buffer = '' + let bytes = 0 + const addRow = (line: string) => { + if (!line.trim()) return + if (rows.length >= 50_000) + throw new ScarfReportError('export too large; choose a shorter window') + rows.push(scarfAggregationRowSchema.parse(JSON.parse(line))) + } + try { + while (true) { + const chunk = await reader.read() + if (chunk.done) break + bytes += chunk.value.byteLength + if (bytes > 100 * 1024 * 1024) + throw new ScarfReportError('export too large; choose a shorter window') + buffer += decoder.decode(chunk.value, { stream: true }) + let end = buffer.indexOf('\n') + while (end !== -1) { + addRow(buffer.slice(0, end)) + buffer = buffer.slice(end + 1) + end = buffer.indexOf('\n') + } + if (buffer.length > 64_000) + throw new ScarfReportError('invalid export row') + } + buffer += decoder.decode() + addRow(buffer) + return rows + } catch (error) { + await reader.cancel() + throw error + } finally { + reader.releaseLock() + } +} + +export async function loadScarfHistory( + days: ScarfHistory['days'], +): Promise { + const window = historyWindow(days) + const base = { + days, + start: window.start, + end: window.end, + fetchedAt: new Date().toISOString(), + companies: [], + issues: [], + } + const host = await getHostRuntimeEnv() + const token = + host?.ANALYTICS_SCARF_API_TOKEN ?? process.env.ANALYTICS_SCARF_API_TOKEN + if (typeof token !== 'string' || !token) + return { ...base, status: 'unconfigured' } + const cache = getDefaultRuntimeCache() + const cacheKey = new Request( + `https://scarf-reporting.tanstack.internal/site-history/v1/${days}/${window.end}`, + ) + const cached = await cache?.match(cacheKey).catch(() => undefined) + if (cached) { + const parsed = scarfHistorySchema.safeParse(await cached.json()) + if (parsed.success) return parsed.data + } + const results = await Promise.allSettled([ + report(window.start, window.end, 'views', token), + report(window.previousStart, window.start, 'views', token), + report(window.start, window.end, 'events', token), + ]) + const data = results.map((result) => + result.status === 'fulfilled' ? result.value : undefined, + ) + const issues = results.flatMap((result, index) => + result.status === 'fulfilled' + ? [] + : [ + `${['Site pixel views', 'Previous period', 'Site events'][index]} unavailable${result.reason instanceof ScarfReportError ? ` (${result.reason.detail})` : ' (unexpected response or timeout)'}. Check Scarf reporting access or try again later.`, + ], + ) + let companies: ScarfHistory['companies'] = [] + try { + companies = companyHistoryFromReports(data[0], data[1], data[2]) + } catch { + issues.push( + 'Scarf returned inconsistent company totals. History is unavailable.', + ) + } + const history: ScarfHistory = { + ...base, + companies, + issues, + status: issues.length + ? companies.length + ? 'partial' + : 'unavailable' + : 'ready', + } + // Cache aggregate company metrics only, never credentials or source-level data. + await cache + ?.put( + cacheKey, + new Response(JSON.stringify(history), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': `max-age=${history.status === 'ready' ? 900 : 60}`, + }, + }), + ) + .catch(() => undefined) + return history +} diff --git a/src/utils/analytics/scarf-history.ts b/src/utils/analytics/scarf-history.ts new file mode 100644 index 000000000..eb3ebdb23 --- /dev/null +++ b/src/utils/analytics/scarf-history.ts @@ -0,0 +1,96 @@ +import { z } from 'zod' + +export const historyDaysSchema = z.union([ + z.literal(7), + z.literal(30), + z.literal(90), + z.literal(365), +]) +export const scarfCompanyHistorySchema = z.object({ + domain: z.string(), + name: z.string().nullable(), + views: z.number().nonnegative().optional(), + previousViews: z.number().nonnegative().optional(), + siteEvents: z.number().nonnegative().optional(), + origins: z.number().nonnegative().optional(), + endpoints: z.number().nonnegative().optional(), + stage: z.string().nullable(), + lastSeen: z.string().nullable(), +}) +export type ScarfCompanyHistory = z.infer +export const scarfHistorySchema = z.object({ + status: z.enum(['ready', 'partial', 'unconfigured', 'unavailable']), + days: historyDaysSchema, + start: z.string(), + end: z.string(), + fetchedAt: z.string(), + companies: z.array(scarfCompanyHistorySchema), + issues: z.array(z.string()), +}) +export type ScarfHistory = z.infer +export function historyWindow(days: number, now = new Date()) { + const end = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1), + ) + const start = new Date(end.getTime() - days * 86_400_000) + const previousStart = new Date(start.getTime() - days * 86_400_000) + return { + start: start.toISOString().slice(0, 10), + end: end.toISOString().slice(0, 10), + previousStart: previousStart.toISOString().slice(0, 10), + } +} +export const scarfAggregationRowSchema = z.object({ + company_domain: z.string().nullable(), + company_name: z.string().nullable(), + total: z.number().int().nonnegative(), + unique_origins: z.number().int().nonnegative(), + unique_endpoints: z.number().int().nonnegative(), + company_funnel_stage: z.string().nullable(), + last_seen: z.string(), +}) +export type ScarfAggregationRow = z.infer +export const scarfAggregationSchema = z.object({ + data: z.array(scarfAggregationRowSchema).max(50_000), +}) + +export function companyHistoryFromReports( + views: z.infer['data'] | undefined, + previous: z.infer['data'] | undefined, + events: z.infer['data'] | undefined, +) { + const companies = new Map() + const reports: { + kind: 'views' | 'previousViews' | 'siteEvents' + rows: typeof views + }[] = [ + { kind: 'views', rows: views }, + { kind: 'previousViews', rows: previous }, + { kind: 'siteEvents', rows: events }, + ] + for (const { kind, rows } of reports) { + const seen = new Set() + for (const row of rows ?? []) { + const domain = row.company_domain?.trim().toLowerCase() + if (!domain) continue + if (seen.has(domain)) + throw new Error('Scarf returned duplicate company totals') + seen.add(domain) + const company = companies.get(domain) ?? { + domain, + name: row.company_name, + stage: null, + lastSeen: null, + } + company[kind] = row.total + if (kind === 'views') { + company.origins = row.unique_origins + company.endpoints = row.unique_endpoints + company.stage = row.company_funnel_stage + company.lastSeen = row.last_seen + } + companies.set(domain, company) + } + } + return [...companies.values()] +} diff --git a/src/utils/analytics/scarf-pixels.ts b/src/utils/analytics/scarf-pixels.ts new file mode 100644 index 000000000..74580fce3 --- /dev/null +++ b/src/utils/analytics/scarf-pixels.ts @@ -0,0 +1,7 @@ +export const homepageScarfPixel = 'b7a8c111-2305-4d95-a02a-d84c5bfb82ae' +export const productScarfPixels = [ + { path: '/application-starter', id: 'ed471ec3-aa08-430f-8481-258b3a1f94dc' }, + { path: '/builder', id: 'aa4dcee9-86a2-49b4-9981-40c97cc1f55b' }, + { path: '/stats/npm', id: 'e729f879-e127-48e1-be19-c68f6786c9e8' }, + { path: '/intent/registry', id: '811c34af-38bd-4f06-9762-3f58dea0650a' }, +] diff --git a/tests/live-company-tools.test.ts b/tests/live-company-tools.test.ts new file mode 100644 index 000000000..d89c66449 --- /dev/null +++ b/tests/live-company-tools.test.ts @@ -0,0 +1,197 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { + buildCompanyGridRows, + filterCompanyRows, + emptyCompanyFilters, + employeeLowerBound, + companyFacetCounts, + companyWidgetItems, + companyGrowth, + csvSafeText, + type LiveOverview, +} from '../src/utils/analytics/live-dashboard.ts' +import { + companyHistoryFromReports, + historyWindow, + type ScarfHistory, +} from '../src/utils/analytics/scarf-history.ts' + +const data: LiveOverview = { + enabled: true, + lookupConfigured: true, + now: '2026-10-29T17:00:00Z', + companies: [ + { + id: 'acme.example', + company: { + name: 'Acme', + domain: 'acme.example', + industry: 'Software', + company_size: '1,000 - 4,999', + linkedin_url: null, + }, + activeSessions: 3, + activeTabs: 5, + visibleTabs: 2, + recentSessions: 4, + pages: ['/query/latest', '/router/latest'], + actions: 9, + recentViews: 12, + lastSeen: new Date('2026-10-29T17:00:00Z'), + }, + { + id: 'other.example', + company: { + name: 'Other', + domain: 'other.example', + industry: 'Retail', + company_size: '100K+', + linkedin_url: null, + }, + activeSessions: 1, + activeTabs: 1, + visibleTabs: 1, + recentSessions: 1, + pages: ['/query/latest'], + actions: 1, + recentViews: 2, + lastSeen: new Date('2026-10-29T17:00:00Z'), + }, + { + id: '', + company: null, + activeSessions: 10, + activeTabs: 10, + visibleTabs: 2, + recentSessions: 10, + pages: ['/form/latest'], + actions: 100, + recentViews: 100, + lastSeen: new Date('2026-10-29T17:00:00Z'), + }, + ], +} +const history: ScarfHistory = { + status: 'ready', + days: 30, + start: '2026-09-30', + end: '2026-10-30', + fetchedAt: data.now, + issues: [], + companies: [ + { + domain: 'acme.example', + name: 'Acme', + views: 500, + previousViews: 250, + siteEvents: 2000, + origins: 12, + endpoints: 18, + stage: 'ongoing-usage', + lastSeen: data.now, + }, + ], +} + +test('facets use OR within a dimension and AND across dimensions', () => { + const rows = buildCompanyGridRows(data, history) + assert.deepEqual( + filterCompanyRows(rows, { + ...emptyCompanyFilters, + library: ['Query', 'Form'], + industry: ['Software'], + }).map((row) => row.id), + ['acme.example'], + ) + assert.deepEqual( + filterCompanyRows(rows, { + ...emptyCompanyFilters, + minViews: '100', + minActions: '5', + }).map((row) => row.id), + ['acme.example'], + ) + assert.equal( + filterCompanyRows(rows, { ...emptyCompanyFilters, identifiedOnly: true }) + .length, + 2, + ) +}) +test('facet counts exclude unmatched traffic and count each company once per library', () => { + const rows = buildCompanyGridRows(data, history) + assert.deepEqual( + companyFacetCounts(rows, 'library').map((item) => [item.name, item.value]), + [ + ['Query', 2], + ['Router', 1], + ], + ) + assert.equal( + companyFacetCounts( + filterCompanyRows( + rows, + { ...emptyCompanyFilters, industry: ['Software'] }, + 'industry', + ), + 'industry', + ).length, + 2, + ) +}) +test('Other retains the actual facet values so a click can filter the grouped categories', () => { + const items = Array.from({ length: 8 }, (_, index) => ({ + name: `Industry ${index}`, + value: 8 - index, + values: [`Industry ${index}`], + })) + const other = companyWidgetItems(items).at(-1) + assert.deepEqual(other, { + name: 'Other', + value: 6, + values: ['Industry 5', 'Industry 6', 'Industry 7'], + }) +}) +test('size sorting handles provider range variants, K suffixes, and unknown values', () => { + assert.equal(employeeLowerBound('1,000 - 4,999'), 1000) + assert.equal(employeeLowerBound('5K-10K'), 5000) + assert.equal(employeeLowerBound('100K+'), 100000) + assert.equal(employeeLowerBound(null), undefined) +}) +test('missing historical metrics stay unknown, zero stays zero, and change uses equal windows', () => { + const rows = buildCompanyGridRows(data, history) + assert.equal(companyGrowth(rows[0]), 100) + assert.equal(rows[1].history, undefined) + assert.equal(companyGrowth(rows[1]), undefined) + assert.deepEqual(historyWindow(30, new Date('2026-10-29T17:00:00Z')), { + start: '2026-09-30', + end: '2026-10-30', + previousStart: '2026-08-31', + }) +}) +test('report joins keep site pixel views separate from telemetry events and do not invent missing values', () => { + const report = { + company_domain: 'ACME.EXAMPLE', + company_name: 'Acme', + total: 12, + unique_origins: 2, + unique_endpoints: 3, + company_funnel_stage: null, + last_seen: data.now, + } + const rows = companyHistoryFromReports([report], undefined, [ + { ...report, total: 90 }, + ]) + assert.equal(rows[0].views, 12) + assert.equal(rows[0].siteEvents, 90) + assert.equal(rows[0].previousViews, undefined) + assert.throws( + () => companyHistoryFromReports([report, report], undefined, undefined), + /duplicate/, + ) +}) +test('CSV text neutralizes spreadsheet formulas while preserving numeric values at their source', () => { + assert.equal(csvSafeText('=SUM(1,2)'), "'=SUM(1,2)") + assert.equal(csvSafeText(' +1'), "' +1") + assert.equal(csvSafeText('Acme'), 'Acme') +}) diff --git a/tests/scarf-history.test.ts b/tests/scarf-history.test.ts new file mode 100644 index 000000000..617218222 --- /dev/null +++ b/tests/scarf-history.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { + loadScarfHistory, + scarfReportUrl, +} from '../src/utils/analytics/scarf-history.server.ts' + +test('site reports select only the website pixels or its dedicated telemetry package', () => { + const views = scarfReportUrl('2026-09-30', '2026-10-30', 'views') + assert.equal(views.pathname, '/v3/insights/TanStack/aggregations/export') + assert.equal(views.searchParams.get('package_id'), 'none') + assert.ok( + views.searchParams + .getAll('tracking_pixel_id') + .includes('b7a8c111-2305-4d95-a02a-d84c5bfb82ae'), + ) + assert.equal(views.searchParams.get('group_by_artifact'), 'false') + assert.equal(views.searchParams.get('group_by_artifact_type'), 'false') + const events = scarfReportUrl('2026-09-30', '2026-10-30', 'events') + assert.equal( + events.searchParams.get('package_id'), + 'b942b596-f90d-4072-a763-86fb5fbe5852', + ) + assert.equal(events.searchParams.get('tracking_pixel_id'), 'none') +}) +test('unavailable reporting access does not pretend the company has zero history', async (t) => { + const previous = process.env.ANALYTICS_SCARF_API_TOKEN + process.env.ANALYTICS_SCARF_API_TOKEN = 'test-token' + t.after(() => { + if (previous === undefined) delete process.env.ANALYTICS_SCARF_API_TOKEN + else process.env.ANALYTICS_SCARF_API_TOKEN = previous + }) + t.mock.method( + globalThis, + 'fetch', + async () => new Response(null, { status: 403 }), + ) + const result = await loadScarfHistory(30) + assert.equal(result.status, 'unavailable') + assert.deepEqual(result.companies, []) + assert.equal(result.issues.length, 3) +}) + +test('streamed history keeps aggregate views separate and strips source identifiers', async (t) => { + const previous = process.env.ANALYTICS_SCARF_API_TOKEN + process.env.ANALYTICS_SCARF_API_TOKEN = 'test-token' + t.after(() => { + if (previous === undefined) delete process.env.ANALYTICS_SCARF_API_TOKEN + else process.env.ANALYTICS_SCARF_API_TOKEN = previous + }) + const { historyWindow } = + await import('../src/utils/analytics/scarf-history.ts') + const window = historyWindow(30) + t.mock.method( + globalThis, + 'fetch', + async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input)) + assert.equal( + new Headers(init?.headers).get('Authorization'), + 'Bearer test-token', + ) + assert.equal(url.searchParams.get('format'), 'ndjson') + const total = + url.searchParams.get('package_id') !== 'none' + ? 90 + : url.searchParams.get('start_date') === window.start + ? 12 + : 6 + const row = { + company_domain: 'example.com', + company_name: 'Example', + total, + unique_origins: 2, + unique_endpoints: 3, + company_funnel_stage: 'ongoing-usage', + last_seen: new Date().toISOString(), + origin_id: 'source-id-must-not-be-returned', + } + return new Response(JSON.stringify(row), { + headers: { 'Content-Type': 'application/x-ndjson' }, + }) + }, + ) + const result = await loadScarfHistory(30) + assert.equal(result.status, 'ready') + assert.equal(result.companies[0].views, 12) + assert.equal(result.companies[0].previousViews, 6) + assert.equal(result.companies[0].siteEvents, 90) + assert.equal( + JSON.stringify(result).includes('source-id-must-not-be-returned'), + false, + ) + assert.equal(JSON.stringify(result).includes('test-token'), false) +}) From 74fb3a7bc4774d0aba0ad95ea8f1626e4d0a950e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 29 Oct 2026 12:15:14 -0600 Subject: [PATCH 04/15] Add active and background company status filters and column --- docs/live-visitors.md | 5 ++ src/components/admin/LiveVisitors.tsx | 69 ++++++++++++++++++++------- src/utils/analytics/live-dashboard.ts | 34 +++++++++---- tests/live-company-tools.test.ts | 59 +++++++++++++++++++++++ 4 files changed, 140 insertions(+), 27 deletions(-) diff --git a/docs/live-visitors.md b/docs/live-visitors.md index c9ad5c002..147170e6f 100644 --- a/docs/live-visitors.md +++ b/docs/live-visitors.md @@ -17,6 +17,11 @@ The overview counts identified companies once per domain. Library breakdowns count a company once per library, so one company can appear in several bars. Industry and size breakdowns exclude unmatched traffic. Clicking a breakdown applies its filter. Size buckets use the lower bound of Scarf's employee range. +The status widget and sortable Status column classify a company as Active when +at least one online tab is visible, Background when all online tabs are hidden, +or Left when no tabs remain online. Each company occupies one status, even when +it has both visible and hidden tabs. Active describes visibility, not confirmed +attention or interaction. Status filters combine with the other company filters. Grid action totals count named interactions in the last five minutes, excluding page views. Pause freezes polling, including expanded sessions and history. diff --git a/src/components/admin/LiveVisitors.tsx b/src/components/admin/LiveVisitors.tsx index c521c9679..1a4068532 100644 --- a/src/components/admin/LiveVisitors.tsx +++ b/src/components/admin/LiveVisitors.tsx @@ -72,13 +72,20 @@ const initialVisibility: ColumnVisibilityState = { } const initialSelection: RowSelectionState = {} const visibilitySchema = z.record(z.string(), z.boolean()) -const facets: CompanyFacet[] = ['library', 'size', 'industry', 'stage'] -const widgetFacets: CompanyFacet[] = ['library', 'size', 'industry'] +const facets: CompanyFacet[] = [ + 'status', + 'library', + 'size', + 'industry', + 'stage', +] +const widgetFacets: CompanyFacet[] = ['status', 'library', 'size', 'industry'] const facetLabels: Record = { library: 'Library', size: 'Company size', industry: 'Industry', stage: 'Funnel stage', + status: 'Status', } const number = new Intl.NumberFormat('en-US') function numeric(value: number | undefined) { @@ -124,6 +131,23 @@ const columns: ColumnDef[] = [ sortFn: sortFn_alphanumeric, enableHiding: false, }, + { + id: 'status', + accessorKey: 'status', + header: 'Status', + sortFn: sortFn_alphanumeric, + cell: ({ row }) => ( + + {row.original.status} + + ), + }, { id: 'industry', accessorKey: 'industry', @@ -268,6 +292,8 @@ const columns: ColumnDef[] = [ }, ] const columnHelp: Record = { + status: + 'Active: at least one online tab is visible. Background: every online tab is hidden. Left: no online tabs. Visibility does not prove attention or interaction.', sessions: 'Anonymous sessions with a heartbeat in the last 65 seconds.', actions: 'Recorded interactions from the last five minutes, limited to 40 retained events per tab.', @@ -446,6 +472,7 @@ export function LiveVisitors({ Employees: csvSafeText(row.company?.company_size || ''), Libraries: csvSafeText(row.libraries.join(', ')), Sessions: row.activeSessions, + Status: row.status, Actions5m: row.actions, RecentViews5m: row.recentViews, SitePixelViews: row.history?.views ?? '', @@ -549,17 +576,19 @@ export function LiveVisitors({ {showBreakdowns && (
{widgetFacets.map((facet) => ( toggleFacet(facet, values)} note={ - facet === 'size' - ? "Grouped by the lower bound of Scarf's reported employee range." - : facet === 'library' - ? 'A company can appear in multiple libraries. Other combines the remaining category counts.' - : undefined + facet === 'status' + ? 'Active means at least one visible tab. Background means all online tabs are hidden. Each company is counted once.' + : facet === 'size' + ? "Grouped by the lower bound of Scarf's reported employee range." + : facet === 'library' + ? 'A company can appear in multiple libraries. Other combines the remaining category counts.' + : undefined } /> ))} diff --git a/src/utils/analytics/live-dashboard.ts b/src/utils/analytics/live-dashboard.ts index 79579bf8f..fc5e8671e 100644 --- a/src/utils/analytics/live-dashboard.ts +++ b/src/utils/analytics/live-dashboard.ts @@ -113,13 +113,14 @@ export function companyBreakdown( export type LiveOverview = Awaited< ReturnType > -export type CompanyFacet = 'library' | 'industry' | 'size' | 'stage' +export type CompanyFacet = 'library' | 'industry' | 'size' | 'stage' | 'status' export type CompanyFilters = { search: string library: string[] industry: string[] size: string[] stage: string[] + status: string[] minViews: string minActions: string showRecent: boolean @@ -131,6 +132,7 @@ export const emptyCompanyFilters: CompanyFilters = { industry: [], size: [], stage: [], + status: [], minViews: '', minActions: '', showRecent: false, @@ -179,6 +181,12 @@ export function buildCompanyGridRows( 'Unmatched traffic', libraries: [...new Set(company.pages.map(pageLibrary))], active: company.activeSessions > 0, + status: + company.visibleTabs > 0 + ? 'Active' + : company.activeTabs > 0 + ? 'Background' + : 'Left', industry: company.company?.industry || 'Unspecified', sizeBucket: companySizeBucket(company.company?.company_size), employeeMin: employeeLowerBound(company.company?.company_size), @@ -188,20 +196,28 @@ export function buildCompanyGridRows( } export type CompanyGridRow = ReturnType[number] export function companyFacetValues(row: CompanyGridRow, facet: CompanyFacet) { - return facet === 'library' - ? row.libraries - : facet === 'industry' - ? [row.industry] - : facet === 'size' - ? [row.sizeBucket] - : [row.history?.stage || 'Unspecified'] + return facet === 'status' + ? [row.status] + : facet === 'library' + ? row.libraries + : facet === 'industry' + ? [row.industry] + : facet === 'size' + ? [row.sizeBucket] + : [row.history?.stage || 'Unspecified'] } export function filterCompanyRows( rows: CompanyGridRow[], filters: CompanyFilters, omit?: CompanyFacet, ) { - const facets: CompanyFacet[] = ['library', 'industry', 'size', 'stage'] + const facets: CompanyFacet[] = [ + 'library', + 'industry', + 'size', + 'stage', + 'status', + ] const search = filters.search.trim().toLowerCase() return rows.filter( (row) => diff --git a/tests/live-company-tools.test.ts b/tests/live-company-tools.test.ts index d89c66449..29df4b3e8 100644 --- a/tests/live-company-tools.test.ts +++ b/tests/live-company-tools.test.ts @@ -195,3 +195,62 @@ test('CSV text neutralizes spreadsheet formulas while preserving numeric values assert.equal(csvSafeText(' +1'), "' +1") assert.equal(csvSafeText('Acme'), 'Acme') }) + +test('company status prefers any visible tab and filters background companies without counting them twice', () => { + const rows = buildCompanyGridRows( + { + ...data, + companies: data.companies.map((company, index) => + index === 1 + ? { ...company, visibleTabs: 0 } + : index === 2 + ? { ...company, activeTabs: 0, activeSessions: 0, visibleTabs: 0 } + : company, + ), + }, + history, + ) + assert.deepEqual( + rows.map((row) => row.status), + ['Active', 'Background', 'Left'], + ) + assert.deepEqual( + companyFacetCounts(rows, 'status').map(({ name, value }) => ({ + name, + value, + })), + [ + { name: 'Active', value: 1 }, + { name: 'Background', value: 1 }, + ], + ) + assert.deepEqual( + filterCompanyRows(rows, { + ...emptyCompanyFilters, + status: ['Background'], + library: ['Query'], + industry: ['Retail'], + }).map((row) => row.id), + ['other.example'], + ) + assert.deepEqual( + filterCompanyRows(rows, { + ...emptyCompanyFilters, + status: ['Active', 'Background'], + }).map((row) => row.id), + ['acme.example', 'other.example'], + ) + assert.equal( + filterCompanyRows(rows, { ...emptyCompanyFilters, status: ['Left'] }) + .length, + 0, + ) + assert.equal( + filterCompanyRows(rows, { + ...emptyCompanyFilters, + status: ['Left'], + showRecent: true, + }).length, + 1, + ) +}) From e50e309bfc87d5cf70dab2a4df118e249ffdaf94 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 29 Oct 2026 13:32:06 -0600 Subject: [PATCH 05/15] Add admin-initiated visitor chat with deferred interface assets --- docs/visitor-chat.md | 98 + drizzle/migrations/0043_visitor_chat.sql | 35 + drizzle/migrations/meta/0043_snapshot.json | 11170 ++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + .../pending-runtime/visitor-chat.test.ts | 314 + src/components/admin/LiveCompanySessions.tsx | 38 +- .../visitor-chat/AdminVisitorChats.css | 4 + .../visitor-chat/AdminVisitorChats.tsx | 411 + src/components/visitor-chat/VisitorChat.css | 4 + src/components/visitor-chat/VisitorChat.tsx | 339 + src/db/schema.ts | 58 + src/instrument.server.mjs | 1 + src/routes/__root.tsx | 16 +- src/routes/admin/live.tsx | 61 +- src/server.ts | 4 + src/server/runtime/host.server.ts | 1 + src/styles/app.css | 1 + src/utils/analytics/live-browser.ts | 51 +- src/utils/analytics/live.functions.ts | 10 +- src/utils/analytics/live.server.ts | 48 +- src/utils/analytics/live.ts | 1 + src/utils/analytics/scarf-lookup.server.ts | 3 + src/utils/analytics/scarf.ts | 10 + src/utils/sentry-redaction.ts | 43 + src/utils/visitor-chat/admin.functions.ts | 64 + src/utils/visitor-chat/auth.server.ts | 56 + src/utils/visitor-chat/bootstrap.ts | 7 + src/utils/visitor-chat/contract.ts | 51 + src/utils/visitor-chat/store.server.ts | 247 + src/utils/visitor-chat/visitor.server.ts | 199 + tests/live-lookup-limit.test.ts | 2 +- tests/live-presence.test.ts | 62 + tests/visitor-chat.test.ts | 270 + wrangler.jsonc | 6 + 34 files changed, 13673 insertions(+), 19 deletions(-) create mode 100644 docs/visitor-chat.md create mode 100644 drizzle/migrations/0043_visitor_chat.sql create mode 100644 drizzle/migrations/meta/0043_snapshot.json create mode 100644 harness-tests/pending-runtime/visitor-chat.test.ts create mode 100644 src/components/visitor-chat/AdminVisitorChats.css create mode 100644 src/components/visitor-chat/AdminVisitorChats.tsx create mode 100644 src/components/visitor-chat/VisitorChat.css create mode 100644 src/components/visitor-chat/VisitorChat.tsx create mode 100644 src/utils/visitor-chat/admin.functions.ts create mode 100644 src/utils/visitor-chat/auth.server.ts create mode 100644 src/utils/visitor-chat/bootstrap.ts create mode 100644 src/utils/visitor-chat/contract.ts create mode 100644 src/utils/visitor-chat/store.server.ts create mode 100644 src/utils/visitor-chat/visitor.server.ts create mode 100644 tests/visitor-chat.test.ts diff --git a/docs/visitor-chat.md b/docs/visitor-chat.md new file mode 100644 index 000000000..5b3b2485d --- /dev/null +++ b/docs/visitor-chat.md @@ -0,0 +1,98 @@ +# Visitor chat + +Visitor chat is disabled by default with `VISITOR_CHAT_ENABLED=false`. The status +column and live-company dashboard remain independently deployed. + +Admins open a company, choose a browser session, and send the first message. +That message opens a small visitor chat using the sender's display name and +profile image. Existing conversations open their transcript directly. Every +admin with access to the live dashboard shares the same inbox and can reply in +the same thread. Senders are assigned from the authenticated server session, +never from browser-provided names or account IDs. No access is granted to new +users by this feature. + +## Loading and delivery + +The existing 20-second presence request carries the invitation signal. Its +response stays empty until a conversation has a committed first message. +Only then does the browser import the visitor chat module and stylesheet. +Chat-specific utilities are excluded from the initial site stylesheet and built +into deferred stylesheets for the visitor window and admin inbox. The shared admin +inbox is also a separate lazy module, opened by Chats or a session's chat action. +The bridge adds no always-open chat connection or additional idle polling loop. +A non-secret browser hint restores an existing chat on reload, including when +presence tracking is off or the visitor is on a private route. The API still +requires the private signed cookie, and the hint never grants access to messages. +The root page contains only shared feature flags, so public HTML caching cannot +cause chat resources to load for another visitor. This does not collect private +page paths. Visitors with a page loaded +before this release must reload before chat becomes +available, and the admin action explains that state. + +Invitations can take up to 20 seconds to appear. Once loaded, visitor and admin +messages poll every two seconds. The browser acknowledges the messages it +actually received, and only marks them seen while the chat is expanded in a +visible tab. These are browser signals, not proof of attention. The admin's +Chats badge and inbox show conversations whose latest message needs a reply. +Chat updates continue independently of pausing the live-company grid. + +Conversation row locks order simultaneous sends. Client message IDs make retries +idempotent. History loads in pages of 100, and older pages remain available. +Messages are plain text. Enter sends and Shift+Enter inserts a line break. +There are no attachments, email delivery, push notifications, or typing indicators +in this version. Leaving the site does not turn a message into an email. + +## Identity and retention + +A random private proof accompanies the existing anonymous presence session. The +server stores its hash. The proof is never returned in admin responses. It binds +the initial invitation to the intended browser, rather than treating a visible +session ID as permission to read messages. + +Once invited, the visitor receives a signed, HttpOnly, Secure, SameSite cookie. +It has a separate signing purpose from account cookies and lasts one year. +Reloads, tabs, and later presence sessions can resume the same conversation. +Clearing cookies, changing browsers, expiration, or signing-key rotation can +remove the visitor's connection to retained history. No IP, fingerprint, or +logged-in visitor account is used to recover it. Conversations target individual +browser sessions, not everyone at the matched company. + +Chat history remains until an admin manually deletes it. Closing a conversation +keeps its history and permits reopening. Stop chat hides the visitor interface, +mutes the conversation, and sets a browser opt-out cookie. Admins cannot reopen +a visitor-muted conversation. Minimizing the window persists across reloads and +does not mark new messages seen. The latest unread messages are counted in its +collapsed header. + +The chat tables do not depend on the 24-hour lifetime of presence records. +Sender names and avatar snapshots stay with their messages if an admin account +changes or is deleted. The company snapshot describes the network at initiation, +not a verified employer, and is shown only to admins. Manual deletion cascades +to all messages and clears presence references. + +## Boundaries + +All admin reads and writes check the current admin capability. Visitor APIs use +the signed chat cookie, not a supplied chat ID, and reject cross-site writes. +The new Cloudflare `VISITOR_CHAT_RATE_LIMITER` binding bounds requests per IP +without storing IPs in the application database. Missing or unavailable limits +fail closed. Message size and request body size are bounded. + +Chat regions are excluded from clipboard analytics, including selections that +span a chat region. Message bodies, cookies, visitor details, and database +parameters are removed from Sentry events and transactions for these endpoints. +Driver write errors are replaced with a message that contains no SQL parameters. +The chat interface does not send message content to Scarf or GA. + +## Release + +1. Apply migration `0043_visitor_chat` through the normal migration flow. +2. Deploy the site Worker with the `VISITOR_CHAT_RATE_LIMITER` binding. The existing + `SESSION_SECRET` signs the separately scoped visitor credential. +3. Verify invitation loading, two different admins in one thread, reload recovery, + manual closure/deletion, visitor opt-out, and cold-page chunk loading. +4. Enable `VISITOR_CHAT_ENABLED=true` when ready to invite production visitors. + +Local integration tests require a disposable, migrated `tanchat_test` PostgreSQL +database, and refuse a production database URL. The preview used synthetic users +and conversations and did not contact real site visitors. diff --git a/drizzle/migrations/0043_visitor_chat.sql b/drizzle/migrations/0043_visitor_chat.sql new file mode 100644 index 000000000..0ca987116 --- /dev/null +++ b/drizzle/migrations/0043_visitor_chat.sql @@ -0,0 +1,35 @@ +CREATE TABLE "visitor_chat_messages" ( + "id" uuid PRIMARY KEY NOT NULL, + "chat_id" uuid NOT NULL, + "sequence" integer NOT NULL, + "sender_id" uuid, + "sender_role" text NOT NULL, + "sender_name" text NOT NULL, + "sender_image" text, + "body" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "visitor_chats" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "origin_session_id" uuid NOT NULL, + "visitor_proof_hash" text NOT NULL, + "company" jsonb, + "next_sequence" integer DEFAULT 0 NOT NULL, + "delivered_through" integer DEFAULT 0 NOT NULL, + "read_through" integer DEFAULT 0 NOT NULL, + "last_visitor_seen_at" timestamp with time zone, + "muted_at" timestamp with time zone, + "closed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "visitor_chats_origin_session_id_unique" UNIQUE("origin_session_id") +); +--> statement-breakpoint +ALTER TABLE "live_sessions" ADD COLUMN "chat_proof_hash" text;--> statement-breakpoint +ALTER TABLE "live_sessions" ADD COLUMN "visitor_chat_id" uuid;--> statement-breakpoint +ALTER TABLE "visitor_chat_messages" ADD CONSTRAINT "visitor_chat_messages_chat_id_visitor_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."visitor_chats"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "visitor_chat_messages" ADD CONSTRAINT "visitor_chat_messages_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "visitor_chat_messages_sequence_idx" ON "visitor_chat_messages" USING btree ("chat_id","sequence");--> statement-breakpoint +CREATE INDEX "visitor_chats_updated_at_idx" ON "visitor_chats" USING btree ("updated_at");--> statement-breakpoint +ALTER TABLE "live_sessions" ADD CONSTRAINT "live_sessions_visitor_chat_id_visitor_chats_id_fk" FOREIGN KEY ("visitor_chat_id") REFERENCES "public"."visitor_chats"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/meta/0043_snapshot.json b/drizzle/migrations/meta/0043_snapshot.json new file mode 100644 index 000000000..4bf14b657 --- /dev/null +++ b/drizzle/migrations/meta/0043_snapshot.json @@ -0,0 +1,11170 @@ +{ + "id": "00ad39cc-d4dd-43d3-ad23-f01085ac9d79", + "prevId": "a833fd20-cc30-4f35-ab72-0ac271d0d37f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_actor_id_idx": { + "name": "audit_logs_actor_id_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_action_idx": { + "name": "audit_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_target_type_idx": { + "name": "audit_logs_target_type_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_target_id_idx": { + "name": "audit_logs_target_id_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_actor_id_users_id_fk": { + "name": "audit_logs_actor_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.builder_project_events": { + "name": "builder_project_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_event_id": { + "name": "client_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "browser_session_id": { + "name": "browser_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "builder_project_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_events_project_created_at_idx": { + "name": "builder_project_events_project_created_at_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_events_run_sequence_idx": { + "name": "builder_project_events_run_sequence_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_events_client_mutation_idx": { + "name": "builder_project_events_client_mutation_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_mutation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_events_project_owner_fk": { + "name": "builder_project_events_project_owner_fk", + "tableFrom": "builder_project_events", + "tableTo": "builder_projects", + "columnsFrom": ["project_id", "owner_id"], + "columnsTo": ["id", "owner_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_events_thread_fk": { + "name": "builder_project_events_thread_fk", + "tableFrom": "builder_project_events", + "tableTo": "builder_project_threads", + "columnsFrom": ["project_id", "thread_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_events_revision_fk": { + "name": "builder_project_events_revision_fk", + "tableFrom": "builder_project_events", + "tableTo": "builder_project_revisions", + "columnsFrom": ["project_id", "revision_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_events_message_fk": { + "name": "builder_project_events_message_fk", + "tableFrom": "builder_project_events", + "tableTo": "builder_project_messages", + "columnsFrom": ["project_id", "message_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_events_run_fk": { + "name": "builder_project_events_run_fk", + "tableFrom": "builder_project_events", + "tableTo": "builder_project_runs", + "columnsFrom": ["project_id", "run_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_events_project_sequence_unique": { + "name": "builder_project_events_project_sequence_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "sequence"] + }, + "builder_project_events_project_client_event_unique": { + "name": "builder_project_events_project_client_event_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "client_event_id"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_project_events_sequence_check": { + "name": "builder_project_events_sequence_check", + "value": "\"builder_project_events\".\"sequence\" BETWEEN 1 AND 9007199254740991" + }, + "builder_project_events_payload_check": { + "name": "builder_project_events_payload_check", + "value": "jsonb_typeof(\"builder_project_events\".\"payload\") = 'object' AND pg_column_size(\"builder_project_events\".\"payload\") <= 262144" + }, + "builder_project_events_entity_reference_check": { + "name": "builder_project_events_entity_reference_check", + "value": "((\"builder_project_events\".\"type\"::text NOT LIKE 'revision.%') OR \"builder_project_events\".\"revision_id\" IS NOT NULL)\n AND ((\"builder_project_events\".\"type\"::text NOT LIKE 'thread.%') OR \"builder_project_events\".\"thread_id\" IS NOT NULL)\n AND ((\"builder_project_events\".\"type\"::text NOT LIKE 'message.%') OR \"builder_project_events\".\"message_id\" IS NOT NULL)\n AND ((\"builder_project_events\".\"type\"::text NOT LIKE 'run.%') OR \"builder_project_events\".\"run_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_legacy_imports": { + "name": "builder_project_legacy_imports", + "schema": "", + "columns": { + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "builder_project_legacy_imports_owner_id_users_id_fk": { + "name": "builder_project_legacy_imports_owner_id_users_id_fk", + "tableFrom": "builder_project_legacy_imports", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.builder_project_messages": { + "name": "builder_project_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "builder_message_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_messages_project_thread_created_at_idx": { + "name": "builder_project_messages_project_thread_created_at_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_messages_run_id_idx": { + "name": "builder_project_messages_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_messages_project_owner_fk": { + "name": "builder_project_messages_project_owner_fk", + "tableFrom": "builder_project_messages", + "tableTo": "builder_projects", + "columnsFrom": ["project_id", "owner_id"], + "columnsTo": ["id", "owner_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_messages_thread_fk": { + "name": "builder_project_messages_thread_fk", + "tableFrom": "builder_project_messages", + "tableTo": "builder_project_threads", + "columnsFrom": ["project_id", "thread_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_messages_run_fk": { + "name": "builder_project_messages_run_fk", + "tableFrom": "builder_project_messages", + "tableTo": "builder_project_runs", + "columnsFrom": ["project_id", "thread_id", "run_id"], + "columnsTo": ["project_id", "thread_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_messages_project_id_unique": { + "name": "builder_project_messages_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "id"] + }, + "builder_project_messages_project_mutation_unique": { + "name": "builder_project_messages_project_mutation_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "client_mutation_id"] + }, + "builder_project_messages_thread_position_unique": { + "name": "builder_project_messages_thread_position_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "thread_id", "position"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_project_messages_parts_check": { + "name": "builder_project_messages_parts_check", + "value": "jsonb_typeof(\"builder_project_messages\".\"parts\") = 'array' AND pg_column_size(\"builder_project_messages\".\"parts\") <= 1048576" + }, + "builder_project_messages_content_size_check": { + "name": "builder_project_messages_content_size_check", + "value": "octet_length(\"builder_project_messages\".\"content\") <= 1048576" + }, + "builder_project_messages_position_check": { + "name": "builder_project_messages_position_check", + "value": "\"builder_project_messages\".\"position\" BETWEEN 1 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_mutation_receipts": { + "name": "builder_project_mutation_receipts", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_type": { + "name": "command_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "builder_project_mutation_receipts_project_fk": { + "name": "builder_project_mutation_receipts_project_fk", + "tableFrom": "builder_project_mutation_receipts", + "tableTo": "builder_projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "builder_project_mutation_receipts_pk": { + "name": "builder_project_mutation_receipts_pk", + "columns": ["project_id", "client_mutation_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "builder_project_mutation_receipts_command_type_check": { + "name": "builder_project_mutation_receipts_command_type_check", + "value": "\"builder_project_mutation_receipts\".\"command_type\" IN ('project.create', 'project.revise', 'project.delete', 'thread.create', 'run.enqueue', 'run.claim', 'run.cancel', 'run.finish', 'run.finish.fallback', 'transcript.import')" + }, + "builder_project_mutation_receipts_request_hash_check": { + "name": "builder_project_mutation_receipts_request_hash_check", + "value": "\"builder_project_mutation_receipts\".\"request_hash\" ~ '^[a-f0-9]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_revisions": { + "name": "builder_project_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_revision_id": { + "name": "parent_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot_hash": { + "name": "snapshot_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_revisions_project_created_at_idx": { + "name": "builder_project_revisions_project_created_at_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_revisions_snapshot_hash_idx": { + "name": "builder_project_revisions_snapshot_hash_idx", + "columns": [ + { + "expression": "snapshot_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_revisions_project_owner_fk": { + "name": "builder_project_revisions_project_owner_fk", + "tableFrom": "builder_project_revisions", + "tableTo": "builder_projects", + "columnsFrom": ["project_id", "owner_id"], + "columnsTo": ["id", "owner_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_revisions_parent_fk": { + "name": "builder_project_revisions_parent_fk", + "tableFrom": "builder_project_revisions", + "tableTo": "builder_project_revisions", + "columnsFrom": ["project_id", "parent_revision_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_revisions_snapshot_hash_fk": { + "name": "builder_project_revisions_snapshot_hash_fk", + "tableFrom": "builder_project_revisions", + "tableTo": "builder_project_snapshots", + "columnsFrom": ["snapshot_hash"], + "columnsTo": ["hash"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_revisions_project_revision_unique": { + "name": "builder_project_revisions_project_revision_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "revision_number"] + }, + "builder_project_revisions_project_id_unique": { + "name": "builder_project_revisions_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "id"] + }, + "builder_project_revisions_project_mutation_unique": { + "name": "builder_project_revisions_project_mutation_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "client_mutation_id"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_project_revisions_snapshot_hash_check": { + "name": "builder_project_revisions_snapshot_hash_check", + "value": "\"builder_project_revisions\".\"snapshot_hash\" ~ '^[a-f0-9]{64}$'" + }, + "builder_project_revisions_revision_number_check": { + "name": "builder_project_revisions_revision_number_check", + "value": "\"builder_project_revisions\".\"revision_number\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_runs": { + "name": "builder_project_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "builder_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queue_kind": { + "name": "queue_kind", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'queue'" + }, + "provider": { + "name": "provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "base_revision_id": { + "name": "base_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result_revision_id": { + "name": "result_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_owner_id": { + "name": "lease_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_fencing_token": { + "name": "lease_fencing_token", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "activity": { + "name": "activity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_runs_project_thread_created_at_idx": { + "name": "builder_project_runs_project_thread_created_at_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_runs_active_lease_idx": { + "name": "builder_project_runs_active_lease_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_runs_active_project_unique": { + "name": "builder_project_runs_active_project_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"builder_project_runs\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_runs_project_owner_fk": { + "name": "builder_project_runs_project_owner_fk", + "tableFrom": "builder_project_runs", + "tableTo": "builder_projects", + "columnsFrom": ["project_id", "owner_id"], + "columnsTo": ["id", "owner_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_runs_thread_fk": { + "name": "builder_project_runs_thread_fk", + "tableFrom": "builder_project_runs", + "tableTo": "builder_project_threads", + "columnsFrom": ["project_id", "thread_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_runs_base_revision_fk": { + "name": "builder_project_runs_base_revision_fk", + "tableFrom": "builder_project_runs", + "tableTo": "builder_project_revisions", + "columnsFrom": ["project_id", "base_revision_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_runs_result_revision_fk": { + "name": "builder_project_runs_result_revision_fk", + "tableFrom": "builder_project_runs", + "tableTo": "builder_project_revisions", + "columnsFrom": ["project_id", "result_revision_id"], + "columnsTo": ["project_id", "id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_runs_project_id_unique": { + "name": "builder_project_runs_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "id"] + }, + "builder_project_runs_project_thread_id_unique": { + "name": "builder_project_runs_project_thread_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "thread_id", "id"] + }, + "builder_project_runs_project_mutation_unique": { + "name": "builder_project_runs_project_mutation_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "client_mutation_id"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_project_runs_queue_kind_check": { + "name": "builder_project_runs_queue_kind_check", + "value": "\"builder_project_runs\".\"queue_kind\" IN ('queue', 'steer')" + }, + "builder_project_runs_fencing_token_check": { + "name": "builder_project_runs_fencing_token_check", + "value": "\"builder_project_runs\".\"lease_fencing_token\" BETWEEN 0 AND 9007199254740991" + }, + "builder_project_runs_lease_pair_check": { + "name": "builder_project_runs_lease_pair_check", + "value": "(\"builder_project_runs\".\"lease_owner_id\" IS NULL) = (\"builder_project_runs\".\"lease_expires_at\" IS NULL)" + }, + "builder_project_runs_running_lease_check": { + "name": "builder_project_runs_running_lease_check", + "value": "(\"builder_project_runs\".\"status\" = 'running') = (\"builder_project_runs\".\"lease_owner_id\" IS NOT NULL)" + }, + "builder_project_runs_terminal_state_check": { + "name": "builder_project_runs_terminal_state_check", + "value": "(\"builder_project_runs\".\"status\" IN ('interrupted', 'completed', 'failed', 'cancelled')) = (\"builder_project_runs\".\"completed_at\" IS NOT NULL)" + }, + "builder_project_runs_timestamp_order_check": { + "name": "builder_project_runs_timestamp_order_check", + "value": "\"builder_project_runs\".\"completed_at\" IS NULL OR \"builder_project_runs\".\"started_at\" IS NULL OR \"builder_project_runs\".\"completed_at\" >= \"builder_project_runs\".\"started_at\"" + }, + "builder_project_runs_activity_size_check": { + "name": "builder_project_runs_activity_size_check", + "value": "\"builder_project_runs\".\"activity\" IS NULL OR (jsonb_typeof(\"builder_project_runs\".\"activity\") = 'object' AND pg_column_size(\"builder_project_runs\".\"activity\") <= 262144)" + }, + "builder_project_runs_error_size_check": { + "name": "builder_project_runs_error_size_check", + "value": "\"builder_project_runs\".\"error\" IS NULL OR (jsonb_typeof(\"builder_project_runs\".\"error\") = 'object' AND pg_column_size(\"builder_project_runs\".\"error\") <= 262144)" + }, + "builder_project_runs_result_revision_check": { + "name": "builder_project_runs_result_revision_check", + "value": "\"builder_project_runs\".\"result_revision_id\" IS NULL OR \"builder_project_runs\".\"status\" = 'completed'" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_snapshot_reservations": { + "name": "builder_project_snapshot_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_hash": { + "name": "snapshot_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_snapshot_reservations_owner_created_at_idx": { + "name": "builder_project_snapshot_reservations_owner_created_at_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_project_snapshot_reservations_snapshot_hash_idx": { + "name": "builder_project_snapshot_reservations_snapshot_hash_idx", + "columns": [ + { + "expression": "snapshot_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_snapshot_reservations_owner_id_users_id_fk": { + "name": "builder_project_snapshot_reservations_owner_id_users_id_fk", + "tableFrom": "builder_project_snapshot_reservations", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "builder_project_snapshot_reservations_snapshot_hash_builder_project_snapshots_hash_fk": { + "name": "builder_project_snapshot_reservations_snapshot_hash_builder_project_snapshots_hash_fk", + "tableFrom": "builder_project_snapshot_reservations", + "tableTo": "builder_project_snapshots", + "columnsFrom": ["snapshot_hash"], + "columnsTo": ["hash"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_snapshot_reservations_owner_snapshot_unique": { + "name": "builder_project_snapshot_reservations_owner_snapshot_unique", + "nullsNotDistinct": false, + "columns": ["owner_id", "snapshot_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.builder_project_snapshots": { + "name": "builder_project_snapshots", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "source_bytes": { + "name": "source_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stored_at": { + "name": "stored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleting_at": { + "name": "deleting_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quarantined_at": { + "name": "quarantined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_project_snapshots_stored_at_idx": { + "name": "builder_project_snapshots_stored_at_idx", + "columns": [ + { + "expression": "stored_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "builder_project_snapshots_hash_check": { + "name": "builder_project_snapshots_hash_check", + "value": "\"builder_project_snapshots\".\"hash\" ~ '^[a-f0-9]{64}$'" + }, + "builder_project_snapshots_source_bytes_check": { + "name": "builder_project_snapshots_source_bytes_check", + "value": "\"builder_project_snapshots\".\"source_bytes\" IS NULL OR \"builder_project_snapshots\".\"source_bytes\" BETWEEN 1 AND 1048576" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_threads": { + "name": "builder_project_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "last_message_position": { + "name": "last_message_position", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "builder_project_threads_project_updated_at_idx": { + "name": "builder_project_threads_project_updated_at_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_threads_project_owner_fk": { + "name": "builder_project_threads_project_owner_fk", + "tableFrom": "builder_project_threads", + "tableTo": "builder_projects", + "columnsFrom": ["project_id", "owner_id"], + "columnsTo": ["id", "owner_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_project_threads_project_id_unique": { + "name": "builder_project_threads_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "id"] + }, + "builder_project_threads_project_mutation_unique": { + "name": "builder_project_threads_project_mutation_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "client_mutation_id"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_project_threads_message_position_check": { + "name": "builder_project_threads_message_position_check", + "value": "\"builder_project_threads\".\"last_message_position\" BETWEEN 0 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.builder_project_tombstones": { + "name": "builder_project_tombstones", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "builder_project_tombstones_owner_deleted_at_idx": { + "name": "builder_project_tombstones_owner_deleted_at_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_project_tombstones_owner_id_users_id_fk": { + "name": "builder_project_tombstones_owner_id_users_id_fk", + "tableFrom": "builder_project_tombstones", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.builder_project_usage": { + "name": "builder_project_usage", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "threads": { + "name": "threads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "messages": { + "name": "messages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "runs": { + "name": "runs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revisions": { + "name": "revisions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "events": { + "name": "events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "payload_bytes": { + "name": "payload_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "builder_project_usage_project_id_builder_projects_id_fk": { + "name": "builder_project_usage_project_id_builder_projects_id_fk", + "tableFrom": "builder_project_usage", + "tableTo": "builder_projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "builder_project_usage_nonnegative_check": { + "name": "builder_project_usage_nonnegative_check", + "value": "\"builder_project_usage\".\"threads\" >= 0 AND \"builder_project_usage\".\"messages\" >= 0 AND \"builder_project_usage\".\"runs\" >= 0 AND \"builder_project_usage\".\"revisions\" >= 0 AND \"builder_project_usage\".\"events\" >= 0 AND \"builder_project_usage\".\"payload_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.builder_projects": { + "name": "builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "forked_from_id": { + "name": "forked_from_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "snapshot_hash": { + "name": "snapshot_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "current_revision_number": { + "name": "current_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_event_sequence": { + "name": "last_event_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_lease_fencing_token": { + "name": "last_lease_fencing_token", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_by_id": { + "name": "deleted_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "builder_projects_owner_id_idx": { + "name": "builder_projects_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_projects_owner_updated_at_idx": { + "name": "builder_projects_owner_updated_at_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "builder_projects_deleted_at_idx": { + "name": "builder_projects_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builder_projects_owner_id_users_id_fk": { + "name": "builder_projects_owner_id_users_id_fk", + "tableFrom": "builder_projects", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "builder_projects_deleted_by_id_users_id_fk": { + "name": "builder_projects_deleted_by_id_users_id_fk", + "tableFrom": "builder_projects", + "tableTo": "users", + "columnsFrom": ["deleted_by_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "builder_projects_forked_from_fk": { + "name": "builder_projects_forked_from_fk", + "tableFrom": "builder_projects", + "tableTo": "builder_projects", + "columnsFrom": ["forked_from_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "builder_projects_snapshot_hash_fk": { + "name": "builder_projects_snapshot_hash_fk", + "tableFrom": "builder_projects", + "tableTo": "builder_project_snapshots", + "columnsFrom": ["snapshot_hash"], + "columnsTo": ["hash"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "builder_projects_id_owner_unique": { + "name": "builder_projects_id_owner_unique", + "nullsNotDistinct": false, + "columns": ["id", "owner_id"] + }, + "builder_projects_owner_mutation_unique": { + "name": "builder_projects_owner_mutation_unique", + "nullsNotDistinct": false, + "columns": ["owner_id", "client_mutation_id"] + } + }, + "policies": {}, + "checkConstraints": { + "builder_projects_snapshot_hash_check": { + "name": "builder_projects_snapshot_hash_check", + "value": "\"builder_projects\".\"snapshot_hash\" ~ '^[a-f0-9]{64}$'" + }, + "builder_projects_revision_number_check": { + "name": "builder_projects_revision_number_check", + "value": "\"builder_projects\".\"current_revision_number\" > 0" + }, + "builder_projects_event_sequence_check": { + "name": "builder_projects_event_sequence_check", + "value": "\"builder_projects\".\"last_event_sequence\" BETWEEN 0 AND 9007199254740991" + }, + "builder_projects_lease_fencing_token_check": { + "name": "builder_projects_lease_fencing_token_check", + "value": "\"builder_projects\".\"last_lease_fencing_token\" BETWEEN 0 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_access": { + "name": "chat_access", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_access_user_id_users_id_fk": { + "name": "chat_access_user_id_users_id_fk", + "tableFrom": "chat_access", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_access_invited_by_users_id_fk": { + "name": "chat_access_invited_by_users_id_fk", + "tableFrom": "chat_access", + "tableTo": "users", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_account_onboarding": { + "name": "chat_account_onboarding", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "use_case": { + "name": "use_case", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_account_onboarding_user_id_users_id_fk": { + "name": "chat_account_onboarding_user_id_users_id_fk", + "tableFrom": "chat_account_onboarding", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_account_onboarding_commands": { + "name": "chat_account_onboarding_commands", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mutation_id": { + "name": "mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_account_onboarding_commands_user_id_chat_account_onboarding_user_id_fk": { + "name": "chat_account_onboarding_commands_user_id_chat_account_onboarding_user_id_fk", + "tableFrom": "chat_account_onboarding_commands", + "tableTo": "chat_account_onboarding", + "columnsFrom": ["user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_account_onboarding_commands_user_id_command_id_pk": { + "name": "chat_account_onboarding_commands_user_id_command_id_pk", + "columns": ["user_id", "command_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_account_preferences": { + "name": "chat_account_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "timezone_confirmed_at": { + "name": "timezone_confirmed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "appearance": { + "name": "appearance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_account_preferences_user_id_users_id_fk": { + "name": "chat_account_preferences_user_id_users_id_fk", + "tableFrom": "chat_account_preferences", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_preferences_revision_safe": { + "name": "chat_preferences_revision_safe", + "value": "\"chat_account_preferences\".\"revision\" >= 0 AND \"chat_account_preferences\".\"revision\" <= 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_bot_drafts": { + "name": "chat_bot_drafts", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "started_at": { + "name": "started_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "run_model": { + "name": "run_model", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reference_inputs": { + "name": "reference_inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "file_ids": { + "name": "file_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_bot_draft_bot_idx": { + "name": "chat_bot_draft_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_bot_draft_scope_fk": { + "name": "chat_bot_draft_scope_fk", + "tableFrom": "chat_bot_drafts", + "tableTo": "chat_file_drafts", + "columnsFrom": ["workspace_id", "user_id", "id"], + "columnsTo": ["workspace_id", "user_id", "id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_bot_draft_conversation_fk": { + "name": "chat_bot_draft_conversation_fk", + "tableFrom": "chat_bot_drafts", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id", "bot_id", "user_id"], + "columnsTo": ["id", "bot_id", "user_id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_bot_draft_workspace_fk": { + "name": "chat_bot_draft_workspace_fk", + "tableFrom": "chat_bot_drafts", + "tableTo": "chat_bots", + "columnsFrom": ["workspace_id", "bot_id"], + "columnsTo": ["workspace_id", "id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_bot_drafts_workspace_id_user_id_id_pk": { + "name": "chat_bot_drafts_workspace_id_user_id_id_pk", + "columns": ["workspace_id", "user_id", "id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_bot_draft_attachments": { + "name": "chat_bot_draft_attachments", + "value": "jsonb_typeof(\"chat_bot_drafts\".\"file_ids\")='array' AND jsonb_array_length(\"chat_bot_drafts\".\"file_ids\")<=5" + } + }, + "isRLSEnabled": false + }, + "public.chat_bot_schedule_suspensions": { + "name": "chat_bot_schedule_suspensions", + "schema": "", + "columns": { + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chat_bot_schedule_suspensions_bot_id_chat_bots_id_fk": { + "name": "chat_bot_schedule_suspensions_bot_id_chat_bots_id_fk", + "tableFrom": "chat_bot_schedule_suspensions", + "tableTo": "chat_bots", + "columnsFrom": ["bot_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_bot_generation_safe": { + "name": "chat_bot_generation_safe", + "value": "\"chat_bot_schedule_suspensions\".\"generation\" BETWEEN 0 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_bot_sections": { + "name": "chat_bot_sections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sort_override": { + "name": "sort_override", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_bot_sections_viewer": { + "name": "chat_bot_sections_viewer", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_bot_sections_workspace_id_chat_workspaces_id_fk": { + "name": "chat_bot_sections_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_bot_sections", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_bot_sections_user_id_users_id_fk": { + "name": "chat_bot_sections_user_id_users_id_fk", + "tableFrom": "chat_bot_sections", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_bot_viewer_state": { + "name": "chat_bot_viewer_state", + "schema": "", + "columns": { + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_bot_viewer_state_bot_id_chat_bots_id_fk": { + "name": "chat_bot_viewer_state_bot_id_chat_bots_id_fk", + "tableFrom": "chat_bot_viewer_state", + "tableTo": "chat_bots", + "columnsFrom": ["bot_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_bot_viewer_state_user_id_users_id_fk": { + "name": "chat_bot_viewer_state_user_id_users_id_fk", + "tableFrom": "chat_bot_viewer_state", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_bot_viewer_state_section_id_chat_bot_sections_id_fk": { + "name": "chat_bot_viewer_state_section_id_chat_bot_sections_id_fk", + "tableFrom": "chat_bot_viewer_state", + "tableTo": "chat_bot_sections", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_bot_viewer_state_bot_id_user_id_pk": { + "name": "chat_bot_viewer_state_bot_id_user_id_pk", + "columns": ["bot_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_bots": { + "name": "chat_bots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deletion_batch": { + "name": "deletion_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_bots_workspace_idx": { + "name": "chat_bots_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_bots_workspace_id_chat_workspaces_id_fk": { + "name": "chat_bots_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_bots", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_bots_workspace_id_parent_id_chat_bots_workspace_id_id_fk": { + "name": "chat_bots_workspace_id_parent_id_chat_bots_workspace_id_id_fk", + "tableFrom": "chat_bots", + "tableTo": "chat_bots", + "columnsFrom": ["workspace_id", "parent_id"], + "columnsTo": ["workspace_id", "id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_bots_workspace_id_unique": { + "name": "chat_bots_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id", "id"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_personal_assistant_active_check": { + "name": "chat_personal_assistant_active_check", + "value": "\"chat_bots\".\"id\" NOT LIKE 'assistant:%' OR (\"chat_bots\".\"archived_at\" IS NULL AND \"chat_bots\".\"deleted_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat_composer_drafts": { + "name": "chat_composer_drafts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_composer_drafts_user_id_users_id_fk": { + "name": "chat_composer_drafts_user_id_users_id_fk", + "tableFrom": "chat_composer_drafts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_composer_drafts_user_id_scope_pk": { + "name": "chat_composer_drafts_user_id_scope_pk", + "columns": ["user_id", "scope"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_draft_revision_safe": { + "name": "chat_draft_revision_safe", + "value": "\"chat_composer_drafts\".\"revision\">0 AND \"chat_composer_drafts\".\"revision\"<=9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_connected_devices": { + "name": "chat_connected_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grants": { + "name": "grants", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen": { + "name": "last_seen", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked_at": { + "name": "revoked_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_devices_owner": { + "name": "chat_devices_owner", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_connected_devices_user_id_users_id_fk": { + "name": "chat_connected_devices_user_id_users_id_fk", + "tableFrom": "chat_connected_devices", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_connected_devices_token_hash_unique": { + "name": "chat_connected_devices_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_conversation_activity": { + "name": "chat_conversation_activity", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_at": { + "name": "activity_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_version": { + "name": "event_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "read_version": { + "name": "read_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "preview": { + "name": "preview", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "message_count": { + "name": "message_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "queued_count": { + "name": "queued_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "queue_paused": { + "name": "queue_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "chat_activity_user_time": { + "name": "chat_activity_user_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_conversation_activity_conversation_id_chat_conversations_id_fk": { + "name": "chat_conversation_activity_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_conversation_activity", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_conversation_activity_bot_id_chat_bots_id_fk": { + "name": "chat_conversation_activity_bot_id_chat_bots_id_fk", + "tableFrom": "chat_conversation_activity", + "tableTo": "chat_bots", + "columnsFrom": ["bot_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_conversation_activity_user_id_users_id_fk": { + "name": "chat_conversation_activity_user_id_users_id_fk", + "tableFrom": "chat_conversation_activity", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_activity_status": { + "name": "chat_activity_status", + "value": "\"chat_conversation_activity\".\"status\" IN ('idle','running','approval','setup','error','completed')" + } + }, + "isRLSEnabled": false + }, + "public.chat_conversation_copies": { + "name": "chat_conversation_copies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "retry_id": { + "name": "retry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_bot_id": { + "name": "source_bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_conversation_id": { + "name": "source_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_bot_id": { + "name": "target_bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_conversation_id": { + "name": "target_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "boundary_json": { + "name": "boundary_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'copying'" + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'export'" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_page": { + "name": "next_page", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "work_version": { + "name": "work_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_copies_pending": { + "name": "chat_copies_pending", + "columns": [ + { + "expression": "source_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_copies_viewer": { + "name": "chat_copies_viewer", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_conversation_copies_retry_id_chat_conversation_retries_id_fk": { + "name": "chat_conversation_copies_retry_id_chat_conversation_retries_id_fk", + "tableFrom": "chat_conversation_copies", + "tableTo": "chat_conversation_retries", + "columnsFrom": ["retry_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_conversation_copies_workspace_id_chat_workspaces_id_fk": { + "name": "chat_conversation_copies_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_conversation_copies", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_conversation_copies_user_id_users_id_fk": { + "name": "chat_conversation_copies_user_id_users_id_fk", + "tableFrom": "chat_conversation_copies", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_conversation_copies_retry_id_unique": { + "name": "chat_conversation_copies_retry_id_unique", + "nullsNotDistinct": false, + "columns": ["retry_id"] + }, + "chat_conversation_copies_target_bot_id_unique": { + "name": "chat_conversation_copies_target_bot_id_unique", + "nullsNotDistinct": false, + "columns": ["target_bot_id"] + }, + "chat_conversation_copies_target_conversation_id_unique": { + "name": "chat_conversation_copies_target_conversation_id_unique", + "nullsNotDistinct": false, + "columns": ["target_conversation_id"] + }, + "chat_conversation_copies_workspace_id_user_id_idempotency_key_unique": { + "name": "chat_conversation_copies_workspace_id_user_id_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id", "user_id", "idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_copy_kind": { + "name": "chat_copy_kind", + "value": "\"chat_conversation_copies\".\"kind\" IN ('duplicate','fork')" + }, + "chat_copy_status": { + "name": "chat_copy_status", + "value": "\"chat_conversation_copies\".\"status\" IN ('copying','ready','failed')" + }, + "chat_copy_phase": { + "name": "chat_copy_phase", + "value": "\"chat_conversation_copies\".\"phase\" IN ('export','transfer','import','publish','cleanup','done')" + } + }, + "isRLSEnabled": false + }, + "public.chat_conversation_mains": { + "name": "chat_conversation_mains", + "schema": "", + "columns": { + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_conversation_mains_conversation_id_bot_id_user_id_chat_conversations_id_bot_id_user_id_fk": { + "name": "chat_conversation_mains_conversation_id_bot_id_user_id_chat_conversations_id_bot_id_user_id_fk", + "tableFrom": "chat_conversation_mains", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id", "bot_id", "user_id"], + "columnsTo": ["id", "bot_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_conversation_mains_bot_id_user_id_pk": { + "name": "chat_conversation_mains_bot_id_user_id_pk", + "columns": ["bot_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_conversation_retries": { + "name": "chat_conversation_retries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_bot_id": { + "name": "source_bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_conversation_id": { + "name": "source_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_json": { + "name": "source_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_plan_json": { + "name": "file_plan_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_retries_viewer": { + "name": "chat_retries_viewer", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_conversation_retries_workspace_id_chat_workspaces_id_fk": { + "name": "chat_conversation_retries_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_conversation_retries", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_conversation_retries_user_id_users_id_fk": { + "name": "chat_conversation_retries_user_id_users_id_fk", + "tableFrom": "chat_conversation_retries", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_conversation_retries_workspace_id_user_id_idempotency_key_unique": { + "name": "chat_conversation_retries_workspace_id_user_id_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id", "user_id", "idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_retry_status": { + "name": "chat_retry_status", + "value": "\"chat_conversation_retries\".\"status\" IN ('preparing','ready','failed')" + } + }, + "isRLSEnabled": false + }, + "public.chat_conversation_threads": { + "name": "chat_conversation_threads", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "parent_conversation_id": { + "name": "parent_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_message_id": { + "name": "source_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_thread_parent_idx": { + "name": "chat_thread_parent_idx", + "columns": [ + { + "expression": "parent_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_thread_child_identity_fk": { + "name": "chat_thread_child_identity_fk", + "tableFrom": "chat_conversation_threads", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id", "bot_id", "user_id"], + "columnsTo": ["id", "bot_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_thread_parent_identity_fk": { + "name": "chat_thread_parent_identity_fk", + "tableFrom": "chat_conversation_threads", + "tableTo": "chat_conversations", + "columnsFrom": ["parent_conversation_id", "bot_id", "user_id"], + "columnsTo": ["id", "bot_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_thread_not_self": { + "name": "chat_thread_not_self", + "value": "\"chat_conversation_threads\".\"conversation_id\" <> \"chat_conversation_threads\".\"parent_conversation_id\"" + }, + "chat_thread_version_nonnegative": { + "name": "chat_thread_version_nonnegative", + "value": "\"chat_conversation_threads\".\"version\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chat_conversations": { + "name": "chat_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_conversations_bot_id_chat_bots_id_fk": { + "name": "chat_conversations_bot_id_chat_bots_id_fk", + "tableFrom": "chat_conversations", + "tableTo": "chat_bots", + "columnsFrom": ["bot_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_conversations_user_id_users_id_fk": { + "name": "chat_conversations_user_id_users_id_fk", + "tableFrom": "chat_conversations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_conversation_identity": { + "name": "chat_conversation_identity", + "nullsNotDistinct": false, + "columns": ["id", "bot_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_credentials": { + "name": "chat_credentials", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_credentials_user_id_users_id_fk": { + "name": "chat_credentials_user_id_users_id_fk", + "tableFrom": "chat_credentials", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_daily_usage": { + "name": "chat_daily_usage", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turns": { + "name": "turns", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "chat_daily_usage_user_id_day_pk": { + "name": "chat_daily_usage_user_id_day_pk", + "columns": ["user_id", "day"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_device_operations": { + "name": "chat_device_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_device_operations_pending": { + "name": "chat_device_operations_pending", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_device_operations_device_id_chat_connected_devices_id_fk": { + "name": "chat_device_operations_device_id_chat_connected_devices_id_fk", + "tableFrom": "chat_device_operations", + "tableTo": "chat_connected_devices", + "columnsFrom": ["device_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_device_operations_user_id_users_id_fk": { + "name": "chat_device_operations_user_id_users_id_fk", + "tableFrom": "chat_device_operations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_device_operations_conversation_id_chat_conversations_id_fk": { + "name": "chat_device_operations_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_device_operations", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_execution_membership_generations": { + "name": "chat_execution_membership_generations", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_execution_membership_generations_workspace_id_chat_workspaces_id_fk": { + "name": "chat_execution_membership_generations_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_execution_membership_generations", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_execution_membership_generations_user_id_users_id_fk": { + "name": "chat_execution_membership_generations_user_id_users_id_fk", + "tableFrom": "chat_execution_membership_generations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_execution_membership_generations_workspace_id_user_id_pk": { + "name": "chat_execution_membership_generations_workspace_id_user_id_pk", + "columns": ["workspace_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_membership_generation_safe": { + "name": "chat_membership_generation_safe", + "value": "\"chat_execution_membership_generations\".\"generation\" BETWEEN 0 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_file_drafts": { + "name": "chat_file_drafts", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_file_drafts_workspace_id_chat_workspaces_id_fk": { + "name": "chat_file_drafts_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_file_drafts", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_file_drafts_user_id_users_id_fk": { + "name": "chat_file_drafts_user_id_users_id_fk", + "tableFrom": "chat_file_drafts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_file_drafts_workspace_id_user_id_id_pk": { + "name": "chat_file_drafts_workspace_id_user_id_id_pk", + "columns": ["workspace_id", "user_id", "id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_funded_spend": { + "name": "chat_funded_spend", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reserved_micros": { + "name": "reserved_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billed_micros": { + "name": "billed_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "observed_micros": { + "name": "observed_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unknown": { + "name": "unknown", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "chat_funded_spend_day_user": { + "name": "chat_funded_spend_day_user", + "columns": [ + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_funded_spend_conversation_id_chat_conversations_id_fk": { + "name": "chat_funded_spend_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_funded_spend", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_funded_spend_user_id_users_id_fk": { + "name": "chat_funded_spend_user_id_users_id_fk", + "tableFrom": "chat_funded_spend", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_funded_spend_conversation_id_run_id_pk": { + "name": "chat_funded_spend_conversation_id_run_id_pk", + "columns": ["conversation_id", "run_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_invites": { + "name": "chat_invites", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_by": { + "name": "redeemed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_invites_creator_idx": { + "name": "chat_invites_creator_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_invites_created_by_users_id_fk": { + "name": "chat_invites_created_by_users_id_fk", + "tableFrom": "chat_invites", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_invites_redeemed_by_users_id_fk": { + "name": "chat_invites_redeemed_by_users_id_fk", + "tableFrom": "chat_invites", + "tableTo": "users", + "columnsFrom": ["redeemed_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_account_probe": { + "name": "chat_kody_account_probe", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_fingerprint": { + "name": "account_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_fingerprint": { + "name": "source_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "checked_at": { + "name": "checked_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_account_probe_user_id_users_id_fk": { + "name": "chat_kody_account_probe_user_id_users_id_fk", + "tableFrom": "chat_kody_account_probe", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_account_reference_catalog": { + "name": "chat_kody_account_reference_catalog", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_fingerprint": { + "name": "account_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_account_reference_catalog_user_id_users_id_fk": { + "name": "chat_kody_account_reference_catalog_user_id_users_id_fk", + "tableFrom": "chat_kody_account_reference_catalog", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_links": { + "name": "chat_kody_links", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_links_user_id_users_id_fk": { + "name": "chat_kody_links_user_id_users_id_fk", + "tableFrom": "chat_kody_links", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_kody_links_subject_unique": { + "name": "chat_kody_links_subject_unique", + "nullsNotDistinct": false, + "columns": ["subject"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_oauth_clients": { + "name": "chat_kody_oauth_clients", + "schema": "", + "columns": { + "origin": { + "name": "origin", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_oauth_pending": { + "name": "chat_kody_oauth_pending", + "schema": "", + "columns": { + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_kody_oauth_pending_expiry": { + "name": "chat_kody_oauth_pending_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_kody_oauth_pending_user_id_users_id_fk": { + "name": "chat_kody_oauth_pending_user_id_users_id_fk", + "tableFrom": "chat_kody_oauth_pending", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_reference_catalog": { + "name": "chat_kody_reference_catalog", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_fingerprint": { + "name": "account_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "complete": { + "name": "complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data_revision": { + "name": "data_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refresh_started_at": { + "name": "refresh_started_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_reference_catalog_user_id_users_id_fk": { + "name": "chat_kody_reference_catalog_user_id_users_id_fk", + "tableFrom": "chat_kody_reference_catalog", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_reference_items": { + "name": "chat_kody_reference_items", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_reference_items_user_id_users_id_fk": { + "name": "chat_kody_reference_items_user_id_users_id_fk", + "tableFrom": "chat_kody_reference_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_kody_reference_items_user_id_revision_ordinal_pk": { + "name": "chat_kody_reference_items_user_id_revision_ordinal_pk", + "columns": ["user_id", "revision", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_refresh_claims": { + "name": "chat_kody_refresh_claims", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "token_fingerprint": { + "name": "token_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claim_id": { + "name": "claim_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_until": { + "name": "lease_until", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_refresh_claims_user_id_users_id_fk": { + "name": "chat_kody_refresh_claims_user_id_users_id_fk", + "tableFrom": "chat_kody_refresh_claims", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_kody_refresh_status": { + "name": "chat_kody_refresh_status", + "value": "\"chat_kody_refresh_claims\".\"status\" IN ('refreshing','completed','needs_auth')" + } + }, + "isRLSEnabled": false + }, + "public.chat_kody_skill_sync": { + "name": "chat_kody_skill_sync", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_fingerprint": { + "name": "account_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_until": { + "name": "lease_until", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_skill_sync_user_id_users_id_fk": { + "name": "chat_kody_skill_sync_user_id_users_id_fk", + "tableFrom": "chat_kody_skill_sync", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_skill_sync_stage": { + "name": "chat_kody_skill_sync_stage", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_kody_skill_sync_stage_user_id_users_id_fk": { + "name": "chat_kody_skill_sync_stage_user_id_users_id_fk", + "tableFrom": "chat_kody_skill_sync_stage", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_kody_skill_sync_stage_user_id_revision_id_pk": { + "name": "chat_kody_skill_sync_stage_user_id_revision_id_pk", + "columns": ["user_id", "revision", "id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_kody_skill_versions": { + "name": "chat_kody_skill_versions", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_skill_id": { + "name": "source_skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "current": { + "name": "current", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_kody_skill_current_idx": { + "name": "chat_kody_skill_current_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_kody_skill_versions_user_id_users_id_fk": { + "name": "chat_kody_skill_versions_user_id_users_id_fk", + "tableFrom": "chat_kody_skill_versions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_kody_skill_versions_user_id_id_version_pk": { + "name": "chat_kody_skill_versions_user_id_id_version_pk", + "columns": ["user_id", "id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_mcp_account_commands": { + "name": "chat_mcp_account_commands", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt": { + "name": "receipt", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_mcp_account_commands_user_id_users_id_fk": { + "name": "chat_mcp_account_commands_user_id_users_id_fk", + "tableFrom": "chat_mcp_account_commands", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_mcp_account_commands_user_id_command_id_pk": { + "name": "chat_mcp_account_commands_user_id_command_id_pk", + "columns": ["user_id", "command_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_mcp_accounts": { + "name": "chat_mcp_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "removed": { + "name": "removed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_revision": { + "name": "token_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_claim": { + "name": "refresh_claim", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_until": { + "name": "refresh_until", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_mcp_owner": { + "name": "chat_mcp_owner", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_mcp_accounts_user_id_users_id_fk": { + "name": "chat_mcp_accounts_user_id_users_id_fk", + "tableFrom": "chat_mcp_accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_mcp_auth_mode": { + "name": "chat_mcp_auth_mode", + "value": "\"chat_mcp_accounts\".\"auth_mode\" IN ('none','token','oauth')" + }, + "chat_mcp_status": { + "name": "chat_mcp_status", + "value": "\"chat_mcp_accounts\".\"status\" IN ('configured','checked','needs_auth','error')" + }, + "chat_mcp_revision": { + "name": "chat_mcp_revision", + "value": "\"chat_mcp_accounts\".\"revision\">0" + }, + "chat_mcp_token_revision": { + "name": "chat_mcp_token_revision", + "value": "\"chat_mcp_accounts\".\"token_revision\">=0" + } + }, + "isRLSEnabled": false + }, + "public.chat_mcp_reference_catalog": { + "name": "chat_mcp_reference_catalog", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "refresh_started_at": { + "name": "refresh_started_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_mcp_reference_catalog_workspace_id_chat_workspaces_id_fk": { + "name": "chat_mcp_reference_catalog_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_mcp_reference_catalog", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_mcp_reference_catalog_user_id_users_id_fk": { + "name": "chat_mcp_reference_catalog_user_id_users_id_fk", + "tableFrom": "chat_mcp_reference_catalog", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_mcp_reference_catalog_workspace_id_user_id_server_id_pk": { + "name": "chat_mcp_reference_catalog_workspace_id_user_id_server_id_pk", + "columns": ["workspace_id", "user_id", "server_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_mcp_setup_attempts": { + "name": "chat_mcp_setup_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_hash": { + "name": "browser_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_mcp_setup_owner": { + "name": "chat_mcp_setup_owner", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_mcp_setup_attempts_user_id_users_id_fk": { + "name": "chat_mcp_setup_attempts_user_id_users_id_fk", + "tableFrom": "chat_mcp_setup_attempts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_mcp_setup_attempts_workspace_id_chat_workspaces_id_fk": { + "name": "chat_mcp_setup_attempts_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_mcp_setup_attempts", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_mcp_setup_attempts_state_hash_unique": { + "name": "chat_mcp_setup_attempts_state_hash_unique", + "nullsNotDistinct": false, + "columns": ["state_hash"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_mcp_setup_status": { + "name": "chat_mcp_setup_status", + "value": "\"chat_mcp_setup_attempts\".\"status\" IN ('review','starting','authorize','complete','failed')" + } + }, + "isRLSEnabled": false + }, + "public.chat_memberships": { + "name": "chat_memberships", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_memberships_user_idx": { + "name": "chat_memberships_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_memberships_workspace_id_chat_workspaces_id_fk": { + "name": "chat_memberships_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_memberships", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_memberships_user_id_users_id_fk": { + "name": "chat_memberships_user_id_users_id_fk", + "tableFrom": "chat_memberships", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_memberships_workspace_id_user_id_pk": { + "name": "chat_memberships_workspace_id_user_id_pk", + "columns": ["workspace_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_memberships_role_check": { + "name": "chat_memberships_role_check", + "value": "\"chat_memberships\".\"role\" IN ('owner', 'admin', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.chat_memories": { + "name": "chat_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_message_id": { + "name": "source_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_memory_scope_idx": { + "name": "chat_memory_scope_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_memories_conversation_id_chat_conversations_id_fk": { + "name": "chat_memories_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_memories", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_memories_workspace_id_chat_workspaces_id_fk": { + "name": "chat_memories_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_memories", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_memories_user_id_users_id_fk": { + "name": "chat_memories_user_id_users_id_fk", + "tableFrom": "chat_memories", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_memory_commands": { + "name": "chat_memory_commands", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_memory_command_identity_idx": { + "name": "chat_memory_command_identity_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_memory_commands_conversation_id_chat_conversations_id_fk": { + "name": "chat_memory_commands_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_memory_commands", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_memory_commands_conversation_id_command_id_pk": { + "name": "chat_memory_commands_conversation_id_command_id_pk", + "columns": ["conversation_id", "command_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_memory_preferences": { + "name": "chat_memory_preferences", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_memory_preferences_conversation_id_chat_conversations_id_fk": { + "name": "chat_memory_preferences_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_memory_preferences", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_bindings": { + "name": "chat_plugin_bindings", + "schema": "", + "columns": { + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requirement_key": { + "name": "requirement_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_bindings_installation_id_version_chat_plugin_versions_installation_id_version_fk": { + "name": "chat_plugin_bindings_installation_id_version_chat_plugin_versions_installation_id_version_fk", + "tableFrom": "chat_plugin_bindings", + "tableTo": "chat_plugin_versions", + "columnsFrom": ["installation_id", "version"], + "columnsTo": ["installation_id", "version"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_plugin_bindings_installation_id_version_requirement_key_pk": { + "name": "chat_plugin_bindings_installation_id_version_requirement_key_pk", + "columns": ["installation_id", "version", "requirement_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_commands": { + "name": "chat_plugin_commands", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt": { + "name": "receipt", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_commands_workspace_id_chat_workspaces_id_fk": { + "name": "chat_plugin_commands_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_plugin_commands", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_plugin_commands_user_id_users_id_fk": { + "name": "chat_plugin_commands_user_id_users_id_fk", + "tableFrom": "chat_plugin_commands", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_plugin_commands_workspace_id_user_id_command_id_pk": { + "name": "chat_plugin_commands_workspace_id_user_id_command_id_pk", + "columns": ["workspace_id", "user_id", "command_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_files": { + "name": "chat_plugin_files", + "schema": "", + "columns": { + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_files_installation_id_version_chat_plugin_versions_installation_id_version_fk": { + "name": "chat_plugin_files_installation_id_version_chat_plugin_versions_installation_id_version_fk", + "tableFrom": "chat_plugin_files", + "tableTo": "chat_plugin_versions", + "columnsFrom": ["installation_id", "version"], + "columnsTo": ["installation_id", "version"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_plugin_files_installation_id_version_path_pk": { + "name": "chat_plugin_files_installation_id_version_path_pk", + "columns": ["installation_id", "version", "path"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_installations": { + "name": "chat_plugin_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_version": { + "name": "current_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "removed": { + "name": "removed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_plugin_owner_list": { + "name": "chat_plugin_owner_list", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_plugin_installations_workspace_id_chat_workspaces_id_fk": { + "name": "chat_plugin_installations_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_plugin_installations", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_plugin_installations_user_id_users_id_fk": { + "name": "chat_plugin_installations_user_id_users_id_fk", + "tableFrom": "chat_plugin_installations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_plugin_version_positive": { + "name": "chat_plugin_version_positive", + "value": "\"chat_plugin_installations\".\"current_version\">0" + }, + "chat_plugin_revision_positive": { + "name": "chat_plugin_revision_positive", + "value": "\"chat_plugin_installations\".\"revision\">0" + } + }, + "isRLSEnabled": false + }, + "public.chat_plugin_skill_identities": { + "name": "chat_plugin_skill_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_skill_identities_installation_id_chat_plugin_installations_id_fk": { + "name": "chat_plugin_skill_identities_installation_id_chat_plugin_installations_id_fk", + "tableFrom": "chat_plugin_skill_identities", + "tableTo": "chat_plugin_installations", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_plugin_skill_path": { + "name": "chat_plugin_skill_path", + "nullsNotDistinct": false, + "columns": ["installation_id", "path"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_skill_versions": { + "name": "chat_plugin_skill_versions", + "schema": "", + "columns": { + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_skill_versions_skill_id_chat_plugin_skill_identities_id_fk": { + "name": "chat_plugin_skill_versions_skill_id_chat_plugin_skill_identities_id_fk", + "tableFrom": "chat_plugin_skill_versions", + "tableTo": "chat_plugin_skill_identities", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_plugin_skill_versions_skill_id_version_pk": { + "name": "chat_plugin_skill_versions_skill_id_version_pk", + "columns": ["skill_id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_plugin_versions": { + "name": "chat_plugin_versions", + "schema": "", + "columns": { + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview": { + "name": "preview", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_plugin_versions_installation_id_chat_plugin_installations_id_fk": { + "name": "chat_plugin_versions_installation_id_chat_plugin_installations_id_fk", + "tableFrom": "chat_plugin_versions", + "tableTo": "chat_plugin_installations", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_plugin_versions_installation_id_version_pk": { + "name": "chat_plugin_versions_installation_id_version_pk", + "columns": ["installation_id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_plugin_saved_version_positive": { + "name": "chat_plugin_saved_version_positive", + "value": "\"chat_plugin_versions\".\"version\">0" + } + }, + "isRLSEnabled": false + }, + "public.chat_recipes": { + "name": "chat_recipes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_recipes_workspace_id_chat_workspaces_id_fk": { + "name": "chat_recipes_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_recipes", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_run_usage_receipts": { + "name": "chat_run_usage_receipts", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled": { + "name": "scheduled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "attempt_id": { + "name": "attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_run_usage_receipts_conversation_id_chat_conversations_id_fk": { + "name": "chat_run_usage_receipts_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_run_usage_receipts", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_run_usage_receipts_workspace_id_chat_workspaces_id_fk": { + "name": "chat_run_usage_receipts_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_run_usage_receipts", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_run_usage_receipts_user_id_users_id_fk": { + "name": "chat_run_usage_receipts_user_id_users_id_fk", + "tableFrom": "chat_run_usage_receipts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_run_usage_receipts_bot_id_chat_bots_id_fk": { + "name": "chat_run_usage_receipts_bot_id_chat_bots_id_fk", + "tableFrom": "chat_run_usage_receipts", + "tableTo": "chat_bots", + "columnsFrom": ["bot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_run_usage_receipts_conversation_id_run_id_pk": { + "name": "chat_run_usage_receipts_conversation_id_run_id_pk", + "columns": ["conversation_id", "run_id"] + } + }, + "uniqueConstraints": { + "chat_run_usage_receipts_attempt_id_unique": { + "name": "chat_run_usage_receipts_attempt_id_unique", + "nullsNotDistinct": false, + "columns": ["attempt_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_saved_file_imports": { + "name": "chat_saved_file_imports", + "schema": "", + "columns": { + "target_file_id": { + "name": "target_file_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_conversation_id": { + "name": "target_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_conversation_id": { + "name": "source_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_file_id": { + "name": "source_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_file_import_target_idx": { + "name": "chat_file_import_target_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_saved_file_imports_workspace_id_chat_workspaces_id_fk": { + "name": "chat_saved_file_imports_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_saved_file_imports", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_saved_file_imports_user_id_users_id_fk": { + "name": "chat_saved_file_imports_user_id_users_id_fk", + "tableFrom": "chat_saved_file_imports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_saved_file_imports_target_conversation_id_chat_conversations_id_fk": { + "name": "chat_saved_file_imports_target_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_saved_file_imports", + "tableTo": "chat_conversations", + "columnsFrom": ["target_conversation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_file_import_size": { + "name": "chat_file_import_size", + "value": "\"chat_saved_file_imports\".\"size\">=0 AND \"chat_saved_file_imports\".\"size\"<=2097152" + }, + "chat_file_import_digest": { + "name": "chat_file_import_digest", + "value": "\"chat_saved_file_imports\".\"sha256\" ~ '^[0-9a-f]{64}$'" + }, + "chat_file_import_source": { + "name": "chat_file_import_source", + "value": "\"chat_saved_file_imports\".\"source\" IN ('upload','assistant')" + } + }, + "isRLSEnabled": false + }, + "public.chat_saved_files": { + "name": "chat_saved_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_id": { + "name": "draft_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_file_conversation_idx": { + "name": "chat_file_conversation_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_file_draft_idx": { + "name": "chat_file_draft_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "draft_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_saved_files_workspace_id_chat_workspaces_id_fk": { + "name": "chat_saved_files_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_saved_files", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_saved_files_user_id_users_id_fk": { + "name": "chat_saved_files_user_id_users_id_fk", + "tableFrom": "chat_saved_files", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_file_draft_fk": { + "name": "chat_file_draft_fk", + "tableFrom": "chat_saved_files", + "tableTo": "chat_file_drafts", + "columnsFrom": ["workspace_id", "user_id", "draft_id"], + "columnsTo": ["workspace_id", "user_id", "id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_file_conversation_fk": { + "name": "chat_file_conversation_fk", + "tableFrom": "chat_saved_files", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id", "bot_id", "user_id"], + "columnsTo": ["id", "bot_id", "user_id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_file_workspace_fk": { + "name": "chat_file_workspace_fk", + "tableFrom": "chat_saved_files", + "tableTo": "chat_bots", + "columnsFrom": ["workspace_id", "bot_id"], + "columnsTo": ["workspace_id", "id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_file_scope": { + "name": "chat_file_scope", + "value": "(\"chat_saved_files\".\"draft_id\" IS NOT NULL AND \"chat_saved_files\".\"bot_id\" IS NULL AND \"chat_saved_files\".\"conversation_id\" IS NULL) OR (\"chat_saved_files\".\"draft_id\" IS NULL AND \"chat_saved_files\".\"bot_id\" IS NOT NULL AND \"chat_saved_files\".\"conversation_id\" IS NOT NULL)" + }, + "chat_file_size": { + "name": "chat_file_size", + "value": "\"chat_saved_files\".\"size\">=0 AND \"chat_saved_files\".\"size\"<=2097152" + }, + "chat_file_name": { + "name": "chat_file_name", + "value": "length(\"chat_saved_files\".\"name\") BETWEEN 1 AND 180" + }, + "chat_file_digest": { + "name": "chat_file_digest", + "value": "\"chat_saved_files\".\"sha256\" ~ '^[0-9a-f]{64}$'" + }, + "chat_file_source": { + "name": "chat_file_source", + "value": "\"chat_saved_files\".\"source\" IN ('upload','assistant')" + }, + "chat_file_state": { + "name": "chat_file_state", + "value": "\"chat_saved_files\".\"state\" IN ('pending','ready')" + } + }, + "isRLSEnabled": false + }, + "public.chat_scheduled_daily_usage": { + "name": "chat_scheduled_daily_usage", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turns": { + "name": "turns", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chat_scheduled_daily_usage_user_id_users_id_fk": { + "name": "chat_scheduled_daily_usage_user_id_users_id_fk", + "tableFrom": "chat_scheduled_daily_usage", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_scheduled_daily_usage_user_id_day_pk": { + "name": "chat_scheduled_daily_usage_user_id_day_pk", + "columns": ["user_id", "day"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_scheduled_turns_nonnegative": { + "name": "chat_scheduled_turns_nonnegative", + "value": "\"chat_scheduled_daily_usage\".\"turns\">=0" + } + }, + "isRLSEnabled": false + }, + "public.chat_skill_commands": { + "name": "chat_skill_commands", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt": { + "name": "receipt", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_skill_commands_workspace_id_chat_workspaces_id_fk": { + "name": "chat_skill_commands_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_skill_commands", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_skill_commands_user_id_users_id_fk": { + "name": "chat_skill_commands_user_id_users_id_fk", + "tableFrom": "chat_skill_commands", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_skill_commands_workspace_id_user_id_command_id_pk": { + "name": "chat_skill_commands_workspace_id_user_id_command_id_pk", + "columns": ["workspace_id", "user_id", "command_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_skill_versions": { + "name": "chat_skill_versions", + "schema": "", + "columns": { + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_skill_versions_skill_id_chat_skills_id_fk": { + "name": "chat_skill_versions_skill_id_chat_skills_id_fk", + "tableFrom": "chat_skill_versions", + "tableTo": "chat_skills", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_skill_versions_skill_id_version_pk": { + "name": "chat_skill_versions_skill_id_version_pk", + "columns": ["skill_id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_skill_saved_version": { + "name": "chat_skill_saved_version", + "value": "\"chat_skill_versions\".\"version\">0 AND \"chat_skill_versions\".\"version\"<=9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_skills": { + "name": "chat_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_skill_owner_list": { + "name": "chat_skill_owner_list", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_skills_workspace_id_chat_workspaces_id_fk": { + "name": "chat_skills_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_skills", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_skills_user_id_users_id_fk": { + "name": "chat_skills_user_id_users_id_fk", + "tableFrom": "chat_skills", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_skill_version": { + "name": "chat_skill_version", + "value": "\"chat_skills\".\"version\">0 AND \"chat_skills\".\"version\"<=9007199254740991" + }, + "chat_skill_revision": { + "name": "chat_skill_revision", + "value": "\"chat_skills\".\"revision\">0 AND \"chat_skills\".\"revision\"<=9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_thread_requests": { + "name": "chat_thread_requests", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_conversation_id": { + "name": "parent_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_message_id": { + "name": "source_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_thread_requests_workspace_id_chat_workspaces_id_fk": { + "name": "chat_thread_requests_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_thread_requests", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_thread_requests_user_id_users_id_fk": { + "name": "chat_thread_requests_user_id_users_id_fk", + "tableFrom": "chat_thread_requests", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_thread_requests_conversation_id_chat_conversations_id_fk": { + "name": "chat_thread_requests_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_thread_requests", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_thread_requests_workspace_id_user_id_idempotency_key_pk": { + "name": "chat_thread_requests_workspace_id_user_id_idempotency_key_pk", + "columns": ["workspace_id", "user_id", "idempotency_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_thread_schedule_suspensions": { + "name": "chat_thread_schedule_suspensions", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chat_thread_schedule_suspensions_conversation_id_chat_conversation_threads_conversation_id_fk": { + "name": "chat_thread_schedule_suspensions_conversation_id_chat_conversation_threads_conversation_id_fk", + "tableFrom": "chat_thread_schedule_suspensions", + "tableTo": "chat_conversation_threads", + "columnsFrom": ["conversation_id"], + "columnsTo": ["conversation_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "chat_thread_generation_safe": { + "name": "chat_thread_generation_safe", + "value": "\"chat_thread_schedule_suspensions\".\"generation\" BETWEEN 0 AND 9007199254740991" + } + }, + "isRLSEnabled": false + }, + "public.chat_waitlist": { + "name": "chat_waitlist", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_waitlist_user_id_users_id_fk": { + "name": "chat_waitlist_user_id_users_id_fk", + "tableFrom": "chat_waitlist", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_workflow_children": { + "name": "chat_workflow_children", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_conversation_id": { + "name": "owner_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admission_json": { + "name": "admission_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_workflow_children_conversation_id_chat_conversations_id_fk": { + "name": "chat_workflow_children_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_workflow_children", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_workflow_children_owner_conversation_id_chat_conversations_id_fk": { + "name": "chat_workflow_children_owner_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_workflow_children", + "tableTo": "chat_conversations", + "columnsFrom": ["owner_conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_workflow_children_owner_conversation_id_workflow_run_id_step_id_unique": { + "name": "chat_workflow_children_owner_conversation_id_workflow_run_id_step_id_unique", + "nullsNotDistinct": false, + "columns": ["owner_conversation_id", "workflow_run_id", "step_id"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_workflow_child_admission_json": { + "name": "chat_workflow_child_admission_json", + "value": "jsonb_typeof(\"chat_workflow_children\".\"admission_json\"::jsonb)='object'" + } + }, + "isRLSEnabled": false + }, + "public.chat_workflow_revisions": { + "name": "chat_workflow_revisions", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_json": { + "name": "definition_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "chat_workflow_revisions_conversation_id_chat_conversations_id_fk": { + "name": "chat_workflow_revisions_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_workflow_revisions", + "tableTo": "chat_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_workflow_revisions_workspace_id_chat_workspaces_id_fk": { + "name": "chat_workflow_revisions_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_workflow_revisions", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chat_workflow_revisions_user_id_users_id_fk": { + "name": "chat_workflow_revisions_user_id_users_id_fk", + "tableFrom": "chat_workflow_revisions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_workflow_revisions_conversation_id_workflow_id_revision_pk": { + "name": "chat_workflow_revisions_conversation_id_workflow_id_revision_pk", + "columns": ["conversation_id", "workflow_id", "revision"] + } + }, + "uniqueConstraints": { + "chat_workflow_revisions_conversation_id_command_id_unique": { + "name": "chat_workflow_revisions_conversation_id_command_id_unique", + "nullsNotDistinct": false, + "columns": ["conversation_id", "command_id"] + } + }, + "policies": {}, + "checkConstraints": { + "chat_workflow_revision_positive": { + "name": "chat_workflow_revision_positive", + "value": "\"chat_workflow_revisions\".\"revision\">0" + } + }, + "isRLSEnabled": false + }, + "public.chat_workspace_sync_clock": { + "name": "chat_workspace_sync_clock", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "published_revision": { + "name": "published_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chat_workspace_sync_clock_workspace_id_chat_workspaces_id_fk": { + "name": "chat_workspace_sync_clock_workspace_id_chat_workspaces_id_fk", + "tableFrom": "chat_workspace_sync_clock", + "tableTo": "chat_workspaces", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_workspace_sync_members": { + "name": "chat_workspace_sync_members", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_workspace_sync_members_workspace_id_user_id_chat_memberships_workspace_id_user_id_fk": { + "name": "chat_workspace_sync_members_workspace_id_user_id_chat_memberships_workspace_id_user_id_fk", + "tableFrom": "chat_workspace_sync_members", + "tableTo": "chat_memberships", + "columnsFrom": ["workspace_id", "user_id"], + "columnsTo": ["workspace_id", "user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "chat_workspace_sync_members_workspace_id_user_id_pk": { + "name": "chat_workspace_sync_members_workspace_id_user_id_pk", + "columns": ["workspace_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_workspaces": { + "name": "chat_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy": { + "name": "policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_workspaces_owner_idx": { + "name": "chat_workspaces_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workspaces_owner_id_users_id_fk": { + "name": "chat_workspaces_owner_id_users_id_fk", + "tableFrom": "chat_workspaces", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doc_feedback": { + "name": "doc_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "doc_feedback_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "page_path": { + "name": "page_path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "library_version": { + "name": "library_version", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "block_selector": { + "name": "block_selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_content_hash": { + "name": "block_content_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "block_markdown": { + "name": "block_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "doc_feedback_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "is_detached": { + "name": "is_detached", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_collapsed": { + "name": "is_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "moderated_at": { + "name": "moderated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "moderation_note": { + "name": "moderation_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_feedback_user_id_idx": { + "name": "doc_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_status_idx": { + "name": "doc_feedback_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_library_idx": { + "name": "doc_feedback_library_idx", + "columns": [ + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_page_path_idx": { + "name": "doc_feedback_page_path_idx", + "columns": [ + { + "expression": "page_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_created_at_idx": { + "name": "doc_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_is_detached_idx": { + "name": "doc_feedback_is_detached_idx", + "columns": [ + { + "expression": "is_detached", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_feedback_moderated_by_idx": { + "name": "doc_feedback_moderated_by_idx", + "columns": [ + { + "expression": "moderated_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doc_feedback_user_id_users_id_fk": { + "name": "doc_feedback_user_id_users_id_fk", + "tableFrom": "doc_feedback", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "doc_feedback_moderated_by_users_id_fk": { + "name": "doc_feedback_moderated_by_users_id_fk", + "tableFrom": "doc_feedback", + "tableTo": "users", + "columnsFrom": ["moderated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_artifact_cache": { + "name": "docs_artifact_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repo": { + "name": "repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "git_ref": { + "name": "git_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "docs_root": { + "name": "docs_root", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "artifact_type": { + "name": "artifact_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stale_at": { + "name": "stale_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_artifact_cache_repo_ref_docs_root_idx": { + "name": "docs_artifact_cache_repo_ref_docs_root_idx", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "docs_root", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_artifact_cache_stale_at_idx": { + "name": "docs_artifact_cache_stale_at_idx", + "columns": [ + { + "expression": "stale_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_artifact_cache_unique": { + "name": "docs_artifact_cache_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "docs_root", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "artifact_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "artifact_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_content_cache": { + "name": "github_content_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repo": { + "name": "repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "git_ref": { + "name": "git_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content_kind": { + "name": "content_kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "is_present": { + "name": "is_present", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "text_content": { + "name": "text_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "json_content": { + "name": "json_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stale_at": { + "name": "stale_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_content_cache_repo_ref_idx": { + "name": "github_content_cache_repo_ref_idx", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_content_cache_stale_at_idx": { + "name": "github_content_cache_stale_at_idx", + "columns": [ + { + "expression": "stale_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_content_cache_unique": { + "name": "github_content_cache_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_stats_cache": { + "name": "github_stats_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cache_key": { + "name": "cache_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "stats": { + "name": "stats", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "previous_stats": { + "name": "previous_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_stats_cache_key_idx": { + "name": "github_stats_cache_key_idx", + "columns": [ + { + "expression": "cache_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_stats_cache_expires_at_idx": { + "name": "github_stats_cache_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_stats_cache_cache_key_unique": { + "name": "github_stats_cache_cache_key_unique", + "nullsNotDistinct": false, + "columns": ["cache_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intent_package_versions": { + "name": "intent_package_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "intent_package_versions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "skill_count": { + "name": "skill_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tarball_url": { + "name": "tarball_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "intent_package_versions_package_name_idx": { + "name": "intent_package_versions_package_name_idx", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_package_versions_sync_status_idx": { + "name": "intent_package_versions_sync_status_idx", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_package_versions_package_version_unique": { + "name": "intent_package_versions_package_version_unique", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "intent_package_versions_package_name_intent_packages_name_fk": { + "name": "intent_package_versions_package_name_intent_packages_name_fk", + "tableFrom": "intent_package_versions", + "tableTo": "intent_packages", + "columnsFrom": ["package_name"], + "columnsTo": ["name"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intent_packages": { + "name": "intent_packages", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intent_skill_content": { + "name": "intent_skill_content", + "schema": "", + "columns": { + "content_hash": { + "name": "content_hash", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intent_skills": { + "name": "intent_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "intent_skills_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "package_version_id": { + "name": "package_version_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "framework": { + "name": "framework", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "requires": { + "name": "requires", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "skill_path": { + "name": "skill_path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "intent_skills_package_version_id_idx": { + "name": "intent_skills_package_version_id_idx", + "columns": [ + { + "expression": "package_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_skills_name_idx": { + "name": "intent_skills_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_skills_framework_idx": { + "name": "intent_skills_framework_idx", + "columns": [ + { + "expression": "framework", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_skills_type_idx": { + "name": "intent_skills_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "intent_skills_package_version_skill_unique": { + "name": "intent_skills_package_version_skill_unique", + "columns": [ + { + "expression": "package_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "intent_skills_package_version_id_intent_package_versions_id_fk": { + "name": "intent_skills_package_version_id_intent_package_versions_id_fk", + "tableFrom": "intent_skills", + "tableTo": "intent_package_versions", + "columnsFrom": ["package_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "intent_skills_content_hash_intent_skill_content_content_hash_fk": { + "name": "intent_skills_content_hash_intent_skill_content_content_hash_fk", + "tableFrom": "intent_skills", + "tableTo": "intent_skill_content", + "columnsFrom": ["content_hash"], + "columnsTo": ["content_hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.live_sessions": { + "name": "live_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "chat_proof_hash": { + "name": "chat_proof_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visitor_chat_id": { + "name": "visitor_chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "company": { + "name": "company", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_status": { + "name": "lookup_status", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "live_sessions_created_at_idx": { + "name": "live_sessions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "live_sessions_visitor_chat_id_visitor_chats_id_fk": { + "name": "live_sessions_visitor_chat_id_visitor_chats_id_fk", + "tableFrom": "live_sessions", + "tableTo": "visitor_chats", + "columnsFrom": ["visitor_chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.live_tabs": { + "name": "live_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "left_at": { + "name": "left_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activity": { + "name": "activity", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "live_tabs_last_seen_idx": { + "name": "live_tabs_last_seen_idx", + "columns": [ + { + "expression": "last_seen", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "live_tabs_session_idx": { + "name": "live_tabs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "live_tabs_session_id_live_sessions_id_fk": { + "name": "live_tabs_session_id_live_sessions_id_fk", + "tableFrom": "live_tabs", + "tableTo": "live_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.login_history": { + "name": "login_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "oauth_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_new_user": { + "name": "is_new_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "login_history_user_id_idx": { + "name": "login_history_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "login_history_created_at_idx": { + "name": "login_history_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "login_history_provider_idx": { + "name": "login_history_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "login_history_user_id_users_id_fk": { + "name": "login_history_user_id_users_id_fk", + "tableFrom": "login_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_api_keys": { + "name": "mcp_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key_hash": { + "name": "key_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rate_limit_per_minute": { + "name": "rate_limit_per_minute", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 200 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_api_keys_key_hash_idx": { + "name": "mcp_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_api_keys_user_id_idx": { + "name": "mcp_api_keys_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_api_keys_is_active_idx": { + "name": "mcp_api_keys_is_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_api_keys_user_id_users_id_fk": { + "name": "mcp_api_keys_user_id_users_id_fk", + "tableFrom": "mcp_api_keys", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mcp_api_keys_key_hash_unique": { + "name": "mcp_api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": ["key_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_rate_limits": { + "name": "mcp_rate_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "mcp_rate_limits_identifier_window_unique": { + "name": "mcp_rate_limits_identifier_window_unique", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_rate_limits_identifier_idx": { + "name": "mcp_rate_limits_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_rate_limits_window_start_idx": { + "name": "mcp_rate_limits_window_start_idx", + "columns": [ + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.npm_download_chunks": { + "name": "npm_download_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "package_name": { + "name": "package_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "date_from": { + "name": "date_from", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "date_to": { + "name": "date_to", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "bin_size": { + "name": "bin_size", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'daily'" + }, + "total_downloads": { + "name": "total_downloads", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "daily_data": { + "name": "daily_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "is_immutable": { + "name": "is_immutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "npm_download_chunks_package_date_bin_unique": { + "name": "npm_download_chunks_package_date_bin_unique", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_from", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_to", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bin_size", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npm_download_chunks_package_name_idx": { + "name": "npm_download_chunks_package_name_idx", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npm_download_chunks_date_from_idx": { + "name": "npm_download_chunks_date_from_idx", + "columns": [ + { + "expression": "date_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npm_download_chunks_date_to_idx": { + "name": "npm_download_chunks_date_to_idx", + "columns": [ + { + "expression": "date_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npm_download_chunks_expires_at_idx": { + "name": "npm_download_chunks_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npm_download_chunks_is_immutable_idx": { + "name": "npm_download_chunks_is_immutable_idx", + "columns": [ + { + "expression": "is_immutable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_mcp_access_tokens": { + "name": "oauth_mcp_access_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'api'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_mcp_access_tokens_hash_idx": { + "name": "oauth_mcp_access_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_mcp_access_tokens_user_idx": { + "name": "oauth_mcp_access_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_mcp_access_tokens_expires_idx": { + "name": "oauth_mcp_access_tokens_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_mcp_access_tokens_user_id_users_id_fk": { + "name": "oauth_mcp_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_mcp_access_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_mcp_access_tokens_token_hash_unique": { + "name": "oauth_mcp_access_tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_accounts": { + "name": "oauth_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "oauth_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_accounts_user_id_idx": { + "name": "oauth_accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_accounts_provider_account_unique": { + "name": "oauth_accounts_provider_account_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_accounts_provider_account_idx": { + "name": "oauth_accounts_provider_account_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_accounts_user_id_users_id_fk": { + "name": "oauth_accounts_user_id_users_id_fk", + "tableFrom": "oauth_accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_mcp_authorization_codes": { + "name": "oauth_mcp_authorization_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'api'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_mcp_auth_codes_hash_idx": { + "name": "oauth_mcp_auth_codes_hash_idx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_mcp_auth_codes_expires_idx": { + "name": "oauth_mcp_auth_codes_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_mcp_authorization_codes_user_id_users_id_fk": { + "name": "oauth_mcp_authorization_codes_user_id_users_id_fk", + "tableFrom": "oauth_mcp_authorization_codes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_mcp_authorization_codes_code_hash_unique": { + "name": "oauth_mcp_authorization_codes_code_hash_unique", + "nullsNotDistinct": false, + "columns": ["code_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_mcp_refresh_tokens": { + "name": "oauth_mcp_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "access_token_id": { + "name": "access_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_mcp_refresh_tokens_hash_idx": { + "name": "oauth_mcp_refresh_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_mcp_refresh_tokens_user_idx": { + "name": "oauth_mcp_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_mcp_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_mcp_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_mcp_refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_mcp_refresh_tokens_access_token_id_oauth_mcp_access_tokens_id_fk": { + "name": "oauth_mcp_refresh_tokens_access_token_id_oauth_mcp_access_tokens_id_fk", + "tableFrom": "oauth_mcp_refresh_tokens", + "tableTo": "oauth_mcp_access_tokens", + "columnsFrom": ["access_token_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_mcp_refresh_tokens_token_hash_unique": { + "name": "oauth_mcp_refresh_tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oss_stats_cache": { + "name": "oss_stats_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scope_type": { + "name": "scope_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "github_star_count": { + "name": "github_star_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "github_contributor_count": { + "name": "github_contributor_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "github_dependent_count": { + "name": "github_dependent_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_fork_count": { + "name": "github_fork_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_repository_count": { + "name": "github_repository_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_delta_star_count": { + "name": "github_delta_star_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_delta_contributor_count": { + "name": "github_delta_contributor_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_delta_dependent_count": { + "name": "github_delta_dependent_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_delta_fork_count": { + "name": "github_delta_fork_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "github_updated_at": { + "name": "github_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "npm_total_downloads": { + "name": "npm_total_downloads", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "npm_rate_per_day": { + "name": "npm_rate_per_day", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "npm_package_count": { + "name": "npm_package_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "npm_updated_at": { + "name": "npm_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "time_delta_ms": { + "name": "time_delta_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oss_stats_cache_scope_unique": { + "name": "oss_stats_cache_scope_unique", + "columns": [ + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oss_stats_cache_scope_type_idx": { + "name": "oss_stats_cache_scope_type_idx", + "columns": [ + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oss_stats_cache_scope_key_idx": { + "name": "oss_stats_cache_scope_key_idx", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_assignments": { + "name": "role_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "role_assignments_user_id_idx": { + "name": "role_assignments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_assignments_role_id_idx": { + "name": "role_assignments_role_id_idx", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_assignments_user_role_unique": { + "name": "role_assignments_user_role_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_assignments_user_id_users_id_fk": { + "name": "role_assignments_user_id_users_id_fk", + "tableFrom": "role_assignments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_assignments_role_id_roles_id_fk": { + "name": "role_assignments_role_id_roles_id_fk", + "tableFrom": "role_assignments", + "tableTo": "roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "capability[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "roles_name_idx": { + "name": "roles_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase_votes": { + "name": "showcase_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "showcase_id": { + "name": "showcase_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcase_votes_showcase_id_idx": { + "name": "showcase_votes_showcase_id_idx", + "columns": [ + { + "expression": "showcase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcase_votes_user_id_idx": { + "name": "showcase_votes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_votes_showcase_id_showcases_id_fk": { + "name": "showcase_votes_showcase_id_showcases_id_fk", + "tableFrom": "showcase_votes", + "tableTo": "showcases", + "columnsFrom": ["showcase_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_votes_user_id_users_id_fk": { + "name": "showcase_votes_user_id_users_id_fk", + "tableFrom": "showcase_votes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_votes_showcase_id_user_id_unique": { + "name": "showcase_votes_showcase_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["showcase_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcases": { + "name": "showcases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tagline": { + "name": "tagline", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screenshot_url": { + "name": "screenshot_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libraries": { + "name": "libraries", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "use_cases": { + "name": "use_cases", + "type": "showcase_use_case[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "showcase_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "placement": { + "name": "placement", + "type": "showcase_placement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "moderated_at": { + "name": "moderated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "moderation_note": { + "name": "moderation_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tranco_rank": { + "name": "tranco_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tranco_rank_updated_at": { + "name": "tranco_rank_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vote_score": { + "name": "vote_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_user_id_idx": { + "name": "showcases_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_status_idx": { + "name": "showcases_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_status_placement_idx": { + "name": "showcases_status_placement_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "placement", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_is_featured_idx": { + "name": "showcases_is_featured_idx", + "columns": [ + { + "expression": "is_featured", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_created_at_idx": { + "name": "showcases_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_moderated_by_idx": { + "name": "showcases_moderated_by_idx", + "columns": [ + { + "expression": "moderated_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_tranco_rank_idx": { + "name": "showcases_tranco_rank_idx", + "columns": [ + { + "expression": "tranco_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "showcases_vote_score_idx": { + "name": "showcases_vote_score_idx", + "columns": [ + { + "expression": "vote_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcases_user_id_users_id_fk": { + "name": "showcases_user_id_users_id_fk", + "tableFrom": "showcases", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcases_moderated_by_users_id_fk": { + "name": "showcases_moderated_by_users_id_fk", + "tableFrom": "showcases", + "tableTo": "users", + "columnsFrom": ["moderated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_activity": { + "name": "user_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_activity_user_date_unique": { + "name": "user_activity_user_date_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_activity_user_id_idx": { + "name": "user_activity_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_activity_date_idx": { + "name": "user_activity_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "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": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_image": { + "name": "oauth_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "capability[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "ads_disabled": { + "name": "ads_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "interested_in_hiding_ads": { + "name": "interested_in_hiding_ads", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_used_framework": { + "name": "last_used_framework", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "signup_sources": { + "name": "signup_sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "session_version": { + "name": "session_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visitor_chat_messages": { + "name": "visitor_chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sender_role": { + "name": "sender_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_name": { + "name": "sender_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_image": { + "name": "sender_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "visitor_chat_messages_sequence_idx": { + "name": "visitor_chat_messages_sequence_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "visitor_chat_messages_chat_id_visitor_chats_id_fk": { + "name": "visitor_chat_messages_chat_id_visitor_chats_id_fk", + "tableFrom": "visitor_chat_messages", + "tableTo": "visitor_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "visitor_chat_messages_sender_id_users_id_fk": { + "name": "visitor_chat_messages_sender_id_users_id_fk", + "tableFrom": "visitor_chat_messages", + "tableTo": "users", + "columnsFrom": ["sender_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visitor_chats": { + "name": "visitor_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "origin_session_id": { + "name": "origin_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visitor_proof_hash": { + "name": "visitor_proof_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company": { + "name": "company", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sequence": { + "name": "next_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_through": { + "name": "delivered_through", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "read_through": { + "name": "read_through", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_visitor_seen_at": { + "name": "last_visitor_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "muted_at": { + "name": "muted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "visitor_chats_updated_at_idx": { + "name": "visitor_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "visitor_chats_origin_session_id_unique": { + "name": "visitor_chats_origin_session_id_unique", + "nullsNotDistinct": false, + "columns": ["origin_session_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "user.capabilities.update", + "user.adsDisabled.update", + "user.sessions.revoke", + "role.create", + "role.update", + "role.delete", + "role.assignment.create", + "role.assignment.delete", + "feedback.moderate", + "showcase.create", + "showcase.update", + "showcase.delete", + "showcase.moderate" + ] + }, + "public.builder_message_role": { + "name": "builder_message_role", + "schema": "public", + "values": ["user", "assistant"] + }, + "public.builder_project_event_type": { + "name": "builder_project_event_type", + "schema": "public", + "values": [ + "project.created", + "project.updated", + "project.deleted", + "thread.created", + "thread.updated", + "thread.archived", + "revision.created", + "message.created", + "message.updated", + "message.deleted", + "run.created", + "run.started", + "run.activity", + "run.interrupted", + "run.completed", + "run.failed", + "run.cancelled" + ] + }, + "public.builder_run_status": { + "name": "builder_run_status", + "schema": "public", + "values": [ + "pending", + "running", + "interrupted", + "completed", + "failed", + "cancelled" + ] + }, + "public.capability": { + "name": "capability", + "schema": "public", + "values": [ + "admin", + "api-keys", + "disableAds", + "builder", + "mcp", + "moderate-feedback", + "moderate-showcases" + ] + }, + "public.doc_feedback_status": { + "name": "doc_feedback_status", + "schema": "public", + "values": ["pending", "approved", "denied"] + }, + "public.doc_feedback_type": { + "name": "doc_feedback_type", + "schema": "public", + "values": ["note", "improvement"] + }, + "public.oauth_provider": { + "name": "oauth_provider", + "schema": "public", + "values": ["github", "google"] + }, + "public.showcase_placement": { + "name": "showcase_placement", + "schema": "public", + "values": ["showcase", "community", "private"] + }, + "public.showcase_status": { + "name": "showcase_status", + "schema": "public", + "values": ["pending", "approved", "denied"] + }, + "public.showcase_use_case": { + "name": "showcase_use_case", + "schema": "public", + "values": [ + "blog", + "e-commerce", + "saas", + "dashboard", + "documentation", + "portfolio", + "social", + "developer-tool", + "marketing", + "media", + "open-source" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 46d6ebc53..5b1233674 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -302,6 +302,13 @@ "when": 1793217188347, "tag": "0042_live_visitors", "breakpoints": true + }, + { + "idx": 43, + "version": "7", + "when": 1793298817210, + "tag": "0043_visitor_chat", + "breakpoints": true } ] } diff --git a/harness-tests/pending-runtime/visitor-chat.test.ts b/harness-tests/pending-runtime/visitor-chat.test.ts new file mode 100644 index 000000000..af8ca762b --- /dev/null +++ b/harness-tests/pending-runtime/visitor-chat.test.ts @@ -0,0 +1,314 @@ +import { afterAll, expect, it } from 'vitest' +import postgres from 'postgres' +import type { AuthUser } from '../../src/auth/types' +import { runWithHostRuntimeEnv } from '../../src/server/runtime/host.server' +import { + recordLiveEvent, + pruneLiveVisitors, + receiveLiveEvent, +} from '../../src/utils/analytics/live.server' +import { type LiveEvent } from '../../src/utils/analytics/live' +import { + chatCookieHeader, + hashChatProof, +} from '../../src/utils/visitor-chat/auth.server' +import { + acknowledgeVisitorChat, + chatsNeedingReply, + appendMessage, + openChatForSession, + readChat, + startAdminChat, +} from '../../src/utils/visitor-chat/store.server' +import { + offerVisitorChat, + receiveVisitorChat, +} from '../../src/utils/visitor-chat/visitor.server' +import { chatStateSchema } from '../../src/utils/visitor-chat/contract' + +const url = new URL(process.env.DATABASE_URL ?? '') +if ( + !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || + !url.pathname.startsWith('/tanchat_test') +) + throw new Error( + 'Visitor chat tests require a disposable local tanchat_test database', + ) +const sql = postgres(url.href, { max: 1 }) +const admin: AuthUser = { + userId: crypto.randomUUID(), + email: 'tanner@example.invalid', + name: 'Tanner', + displayUsername: 'Tanner', + image: 'https://example.invalid/tanner.jpg', + oauthImage: null, + capabilities: ['admin'], + adsDisabled: null, + interestedInHidingAds: null, + lastUsedFramework: null, + signupSources: [], +} +const jonny: AuthUser = { + ...admin, + userId: crypto.randomUUID(), + name: 'Jonny', + displayUsername: 'Jonny', + image: 'https://example.invalid/jonny.jpg', +} +const event: LiveEvent = { + sessionId: crypto.randomUUID(), + tabId: crypto.randomUUID(), + sequence: 1, + page: '/query/latest', + visible: true, + kind: 'update', + chatProof: crypto.randomUUID(), +} +const ids: string[] = [] +const sessionIds = [event.sessionId] +const environment = { + VISITOR_CHAT_ENABLED: 'true', + LIVE_VISITORS_ENABLED: 'true', + SESSION_SECRET: 'local-chat-test-secret', + LIVE_PRESENCE_RATE_LIMITER: { limit: async () => ({ success: true }) }, + VISITOR_CHAT_RATE_LIMITER: { limit: async () => ({ success: true }) }, +} +const message = (body: string) => ({ id: crypto.randomUUID(), body }) +const visitorRequest = (cookie: string, body?: unknown) => + new Request('https://tanstack.com/_a/visitor-chat', { + method: body ? 'POST' : 'GET', + headers: { + cookie, + origin: 'https://tanstack.com', + 'cf-connecting-ip': '192.0.2.1', + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) +afterAll(async () => { + for (const id of ids) await sql`DELETE FROM visitor_chats WHERE id=${id}` + for (const id of sessionIds) + await sql`DELETE FROM live_sessions WHERE id=${id}` + await sql`DELETE FROM users WHERE id IN (${admin.userId},${jonny.userId})` + await sql.end() +}) +it('keeps a private, shared, durable conversation through concurrent admins, retries, reloads and session expiry', async () => { + await runWithHostRuntimeEnv(environment, async () => { + const pendingBefore = await chatsNeedingReply() + await sql`INSERT INTO users(id) VALUES (${admin.userId}),(${jonny.userId})` + await expect( + startAdminChat(event.sessionId, message('unauthenticated')), + ).rejects.toThrow() + await recordLiveEvent(event, '192.0.2.1', undefined, undefined) + const baseline = new Request('https://tanstack.com/_a/live', { + method: 'POST', + headers: { + origin: 'https://tanstack.com', + 'cf-connecting-ip': '192.0.2.1', + }, + body: JSON.stringify({ ...event, sequence: 2 }), + }) + expect((await receiveLiveEvent(baseline)).status).toBe(204) + const [first, second] = await Promise.all([ + openChatForSession(event.sessionId, message('Tanner opening'), admin), + openChatForSession(event.sessionId, message('Jonny joining'), jonny), + ]) + expect(first.id).toBe(second.id) + ids.push(first.id) + const chatId = first.id + const announcement = await receiveLiveEvent( + new Request('https://tanstack.com/_a/live', { + method: 'POST', + headers: { + origin: 'https://tanstack.com', + 'cf-connecting-ip': '192.0.2.1', + }, + body: JSON.stringify({ ...event, sequence: 3 }), + }), + ) + expect(announcement.status).toBe(200) + expect(announcement.headers.getSetCookie()).toHaveLength(2) + expect( + announcement.headers + .getSetCookie() + .some((value) => value.startsWith('__Host-tanstack_chat_started=1;')), + ).toBe(true) + const shared = await readChat(chatId) + expect(shared.messages.map((m) => m.sequence)).toEqual([1, 2]) + expect(new Set(shared.messages.map((m) => m.senderName))).toEqual( + new Set(['Tanner', 'Jonny']), + ) + expect( + shared.messages.every((m) => + m.senderImage?.startsWith('https://example.invalid/'), + ), + ).toBe(true) + const wrongProof = { ...event, chatProof: crypto.randomUUID() } + expect( + await offerVisitorChat( + new Request('https://tanstack.com/_a/live'), + wrongProof, + false, + ), + ).toBeNull() + const offer = await offerVisitorChat( + new Request('https://tanstack.com/_a/live'), + event, + false, + ) + expect(offer?.chat).toBe(true) + const cookie = offer!.cookie!.split(';')[0] + const loaded = await receiveVisitorChat(visitorRequest(cookie)) + expect(loaded.status).toBe(200) + const body = await loaded.text() + expect(body).not.toContain('visitorProofHash') + expect(body).not.toContain(event.chatProof!) + expect(body).not.toContain(admin.email) + expect(body).not.toContain('senderId') + const reply = message('visitor reply') + const sent = await receiveVisitorChat( + visitorRequest(cookie, { + action: 'send', + message: { ...reply, senderRole: 'admin', senderName: 'Tanner' }, + chatId: crypto.randomUUID(), + }), + ) + expect(sent.status).toBe(200) + expect(await chatsNeedingReply()).toBe(pendingBefore + 1) + const sentState = chatStateSchema.parse(await sent.json()) + expect(sentState.messages.at(-1)?.senderRole).toBe('visitor') + expect(sentState.messages.at(-1)?.senderName).toBe('Visitor') + await receiveVisitorChat( + visitorRequest(cookie, { action: 'send', message: reply }), + ) + expect((await readChat(chatId)).messages).toHaveLength(3) + const next = await appendMessage(chatId, message('admin after snapshot'), { + senderRole: 'admin', + senderId: admin.userId, + senderName: 'Tanner', + senderImage: null, + }) + expect(next.messages).toHaveLength(4) + expect(await chatsNeedingReply()).toBe(pendingBefore) + await acknowledgeVisitorChat(chatId, 3, 3) + expect((await readChat(chatId)).deliveredThrough).toBe(3) + expect((await readChat(chatId)).readThrough).toBe(3) + const reloaded = chatStateSchema.parse( + await (await receiveVisitorChat(visitorRequest(cookie))).json(), + ) + expect(reloaded.messages).toHaveLength(4) + const otherId = crypto.randomUUID() + ids.push(otherId) + await sql`INSERT INTO visitor_chats(id,origin_session_id,visitor_proof_hash) VALUES (${otherId},${crypto.randomUUID()},${await hashChatProof(crypto.randomUUID())})` + const otherCookie = (await chatCookieHeader(otherId)).split(';')[0] + const otherState = chatStateSchema.parse( + await (await receiveVisitorChat(visitorRequest(otherCookie))).json(), + ) + expect(otherState.messages).toHaveLength(0) + await expect( + appendMessage( + otherId, + { id: reply.id, body: 'private-text-must-not-appear-in-errors' }, + { + senderRole: 'visitor', + senderId: null, + senderName: 'Visitor', + senderImage: null, + }, + ), + ).rejects.toThrow('Could not save the message. Please try again.') + await sql`UPDATE live_sessions SET created_at=now()-interval '2 days' WHERE id=${event.sessionId}` + await pruneLiveVisitors(Date.now()) + expect((await readChat(chatId)).messages).toHaveLength(4) + expect((await receiveVisitorChat(visitorRequest(cookie))).status).toBe(200) + const stopped = await receiveVisitorChat( + visitorRequest(cookie, { action: 'stop' }), + ) + expect(stopped.status).toBe(204) + expect( + stopped.headers + .getSetCookie() + .some((value) => value.startsWith('__Host-tanstack_chat_started=;')), + ).toBe(true) + expect(stopped.headers.get('set-cookie')).toContain( + 'visitor_chat_stopped=1', + ) + expect( + ( + await receiveVisitorChat( + visitorRequest(cookie, { + action: 'send', + message: message('blocked'), + }), + ) + ).status, + ).toBe(409) + expect( + await offerVisitorChat( + new Request('https://tanstack.com/_a/live', { headers: { cookie } }), + event, + false, + ), + ).toBeNull() + await sql`DELETE FROM visitor_chats WHERE id=${chatId}` + const [remaining] = + await sql`SELECT count(*)::int AS count FROM visitor_chat_messages WHERE chat_id=${chatId}` + expect(remaining.count).toBe(0) + expect((await receiveVisitorChat(visitorRequest(cookie))).status).toBe(404) + const fresh = { + ...event, + sessionId: crypto.randomUUID(), + tabId: crypto.randomUUID(), + chatProof: crypto.randomUUID(), + } + sessionIds.push(fresh.sessionId) + await recordLiveEvent(fresh, '192.0.2.1', undefined, undefined) + const replacement = await openChatForSession( + fresh.sessionId, + message('New conversation after deletion'), + admin, + ) + ids.push(replacement.id) + const replacementOffer = await offerVisitorChat( + new Request('https://tanstack.com/_a/live', { headers: { cookie } }), + fresh, + false, + ) + expect(replacementOffer?.cookie).toBeTruthy() + const replacementCookie = replacementOffer!.cookie!.split(';')[0] + expect( + chatStateSchema.parse( + await ( + await receiveVisitorChat(visitorRequest(replacementCookie)) + ).json(), + ).id, + ).toBe(replacement.id) + expect( + await offerVisitorChat( + new Request('https://tanstack.com/_a/live', { + headers: { + cookie: `${replacementCookie}; __Host-tanstack_visitor_chat_stopped=1`, + }, + }), + fresh, + false, + ), + ).toBeNull() + }) +}) + +it('pages through retained history without truncating it permanently', async () => { + const id = crypto.randomUUID() + ids.push(id) + await sql`INSERT INTO visitor_chats(id,origin_session_id,visitor_proof_hash,next_sequence) VALUES (${id},${crypto.randomUUID()},${await hashChatProof(crypto.randomUUID())},205)` + await sql`INSERT INTO visitor_chat_messages(id,chat_id,sequence,sender_role,sender_name,body) SELECT gen_random_uuid(),${id},n,'visitor','Visitor','Synthetic history message' FROM generate_series(1,205) n` + const recent = await readChat(id) + expect(recent.messages).toHaveLength(100) + expect(recent.messages[0].sequence).toBe(106) + expect(recent.hasOlder).toBe(true) + const middle = await readChat(id, 106) + expect(middle.messages[0].sequence).toBe(6) + const first = await readChat(id, 6) + expect(first.messages.map((m) => m.sequence)).toEqual([1, 2, 3, 4, 5]) + expect(first.hasOlder).toBe(false) +}) diff --git a/src/components/admin/LiveCompanySessions.tsx b/src/components/admin/LiveCompanySessions.tsx index 34ed7614a..08afe85b8 100644 --- a/src/components/admin/LiveCompanySessions.tsx +++ b/src/components/admin/LiveCompanySessions.tsx @@ -70,6 +70,7 @@ export function LiveCompanySessions({ sessions.map((tabs) => ( @@ -81,7 +82,15 @@ export function LiveCompanySessions({ ) } -function SessionDetails({ tabs, now }: { tabs: LiveTab[]; now: string }) { +function SessionDetails({ + tabs, + now, + chatEnabled, +}: { + tabs: LiveTab[] + now: string + chatEnabled: boolean +}) { const first = tabs[0] const current = tabs.find((tab) => tab.active && tab.visible) ?? @@ -104,6 +113,33 @@ function SessionDetails({ tabs, now }: { tabs: LiveTab[]; now: string }) { className="hidden text-gray-400 group-open/session:block" /> Visitor {first.sessionId.slice(0, 8)} + {chatEnabled && active && ( + + )} diff --git a/src/components/visitor-chat/AdminVisitorChats.css b/src/components/visitor-chat/AdminVisitorChats.css new file mode 100644 index 000000000..c18cd160d --- /dev/null +++ b/src/components/visitor-chat/AdminVisitorChats.css @@ -0,0 +1,4 @@ +@import 'tailwindcss/utilities.css' layer(utilities) source(none); +@reference 'tailwindcss/theme.css'; +@custom-variant dark (&:is(.dark, .dark *)); +@source './AdminVisitorChats.tsx'; diff --git a/src/components/visitor-chat/AdminVisitorChats.tsx b/src/components/visitor-chat/AdminVisitorChats.tsx new file mode 100644 index 000000000..164aca7b7 --- /dev/null +++ b/src/components/visitor-chat/AdminVisitorChats.tsx @@ -0,0 +1,411 @@ +import { useEffect, useRef, useState } from 'react' +import './AdminVisitorChats.css' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { + closeVisitorChat, + deleteVisitorChat, + getVisitorChat, + getVisitorChats, + reopenVisitorChat, + sendVisitorChat, + startVisitorChat, +} from '~/utils/visitor-chat/admin.functions' +import { + chatPollMs, + chatStateSchema, + mergeChatState, + type VisitorChatState, +} from '~/utils/visitor-chat/contract' + +export default function AdminVisitorChats({ + target, + onClose, +}: { + target: { sessionId: string; label: string; chatId?: string | null } | null + onClose: () => void +}) { + const client = useQueryClient() + const [selected, setSelected] = useState(target?.chatId || '') + const [selectedLabel, setSelectedLabel] = useState(target?.label || '') + const [search, setSearch] = useState('') + const [includeClosed, setIncludeClosed] = useState(false) + const [chat, setChat] = useState((): VisitorChatState | undefined => { + const cached = chatStateSchema.safeParse( + client.getQueryData(['admin', 'visitor-chat', selected]), + ) + return cached.success ? cached.data : undefined + }) + const [draft, setDraft] = useState('') + const [sending, setSending] = useState(false) + const [error, setError] = useState('') + const pending: { current: { id: string; body: string } | undefined } = + useRef(undefined) + const tail: { current: HTMLDivElement | null } = useRef(null) + const inbox = useQuery({ + queryKey: ['admin', 'visitor-chats', search, includeClosed], + queryFn: () => getVisitorChats({ data: { search, includeClosed } }), + refetchInterval: chatPollMs, + retry: false, + }) + const thread = useQuery({ + queryKey: ['admin', 'visitor-chat', selected], + queryFn: () => getVisitorChat({ data: { chatId: selected } }), + enabled: Boolean(selected), + refetchInterval: chatPollMs, + retry: false, + }) + useEffect(() => { + setChat(undefined) + setDraft('') + setError('') + pending.current = undefined + }, [selected]) + useEffect(() => { + const next = thread.data + if (next) setChat((previous) => mergeChatState(previous, next)) + else if (thread.isSuccess && next === null) { + setChat(undefined) + setSelected('') + void client.invalidateQueries({ queryKey: ['admin', 'visitor-chats'] }) + } + }, [thread.data, thread.isSuccess, client]) + const lastSequence = chat?.messages.at(-1)?.sequence + useEffect(() => { + tail.current?.scrollIntoView({ block: 'nearest' }) + }, [lastSequence]) + const send = async () => { + const body = draft.trim() + if (!body || sending || (!selected && !target)) return + const message = + pending.current?.body === body + ? pending.current + : { id: crypto.randomUUID(), body } + pending.current = message + setSending(true) + setError('') + try { + const next = selected + ? await sendVisitorChat({ data: { chatId: selected, message } }) + : target + ? await startVisitorChat({ + data: { sessionId: target.sessionId, message }, + }) + : undefined + if (next) { + setSelected(next.id) + setChat((previous) => mergeChatState(previous, next)) + client.setQueryData(['admin', 'visitor-chat', next.id], next) + } + setDraft('') + pending.current = undefined + void client.invalidateQueries({ queryKey: ['admin', 'visitor-chats'] }) + } catch (error) { + setError( + error instanceof Error ? error.message : 'Could not send. Try again.', + ) + } finally { + setSending(false) + } + } + const label = selected + ? selectedLabel || `Visitor ${selected.slice(0, 8)}` + : target?.label || 'Select a chat' + return ( +