diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 765920716b2..7985328c089 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1,6 +1,22 @@ import type { SVGProps } from 'react' import { useId } from 'react' +export function EnrichmentIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function AgentMailIcon(props: SVGProps) { return ( @@ -4731,7 +4747,7 @@ export function ZoomInfoIcon(props: SVGProps) { return (

+ {landingContent.install.heading} +

+

+ {landingContent.install.intro} +

+
    + {landingContent.install.steps.map((item, index) => ( +
  1. + +
    +

    + {item.title} +

    +

    + {item.body} +

    +
    +
  2. + ))} +
+
+ + Add to {name} + +
+ +
+ + )} + + {/* Privacy & data (integration-specific) */} + {landingContent?.privacy && ( + <> +
+

+ Privacy & data +

+

+ {landingContent.privacy.body}{' '} + + Privacy Policy + + . +

+
+
+ + )} + {/* How to automate */}

= { ashby: AshbyIcon, athena: AthenaIcon, attio: AttioIcon, - azure_devops: AzureDevOpsIcon, + azure_devops: AzureIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brightdata: BrightDataIcon, @@ -262,6 +262,7 @@ export const blockTypeToIconMap: Record = { elevenlabs: ElevenLabsIcon, emailbison: EmailBisonIcon, enrich: EnrichSoIcon, + enrichment: EnrichmentIcon, evernote: EvernoteIcon, exa: ExaAIIcon, extend_v2: ExtendIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index d859c6c3223..8fc5c3856ff 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -1889,7 +1889,7 @@ "description": "Interact with Azure DevOps pipelines, builds, and work items", "longDescription": "Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.", "bgColor": "#0078D4", - "iconName": "AzureDevOpsIcon", + "iconName": "AzureIcon", "docsUrl": "https://docs.sim.ai/tools/azure_devops", "operations": [ { @@ -3137,6 +3137,24 @@ "integrationTypes": ["analytics", "developer-tools"], "tags": ["data-analytics", "automation"] }, + { + "type": "enrichment", + "slug": "data-enrichment", + "name": "Data Enrichment", + "description": "Enrich data with a Sim enrichment", + "longDescription": "Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.", + "bgColor": "#9333EA", + "iconName": "EnrichmentIcon", + "docsUrl": "https://docs.sim.ai/tools/enrichment", + "operations": [], + "operationCount": 0, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment"] + }, { "type": "databricks", "slug": "databricks", @@ -13288,7 +13306,35 @@ "authType": "oauth", "category": "tools", "integrationTypes": ["communication", "developer-tools"], - "tags": ["messaging", "webhooks", "automation"] + "tags": ["messaging", "webhooks", "automation"], + "landingContent": { + "install": { + "heading": "Add Sim to your Slack workspace", + "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", + "steps": [ + { + "title": "Create your free Sim account", + "body": "Sign up at sim.ai — no credit card required." + }, + { + "title": "Add a Slack block", + "body": "Open a workflow, drag in a Slack block, and open its credential dropdown." + }, + { + "title": "Connect Slack", + "body": "Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace." + }, + { + "title": "Invite the bot and build", + "body": "Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run." + } + ] + }, + "privacy": { + "body": "Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.", + "href": "/privacy" + } + } }, { "type": "smtp", diff --git a/apps/sim/app/(landing)/integrations/data/landing-content.ts b/apps/sim/app/(landing)/integrations/data/landing-content.ts new file mode 100644 index 00000000000..7004c1f785a --- /dev/null +++ b/apps/sim/app/(landing)/integrations/data/landing-content.ts @@ -0,0 +1,41 @@ +/** + * Hand-authored, integration-specific landing content, keyed by integration + * slug. This is a pure-data generation input: `scripts/generate-docs.ts` reads + * it and bakes the matching entry into `integrations.json`, so the landing page + * consumes a single source (`integration.landingContent`) with no render-time + * augmentation. Has no app imports so the build script can import it safely. + */ + +import type { IntegrationLandingContent } from '@/app/(landing)/integrations/data/types' + +export const INTEGRATION_LANDING_CONTENT: Record = { + slack: { + install: { + heading: 'Add Sim to your Slack workspace', + intro: + 'Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.', + steps: [ + { + title: 'Create your free Sim account', + body: 'Sign up at sim.ai — no credit card required.', + }, + { + title: 'Add a Slack block', + body: 'Open a workflow, drag in a Slack block, and open its credential dropdown.', + }, + { + title: 'Connect Slack', + body: 'Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace.', + }, + { + title: 'Invite the bot and build', + body: 'Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run.', + }, + ], + }, + privacy: { + body: 'Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.', + href: '/privacy', + }, + }, +} diff --git a/apps/sim/app/(landing)/integrations/data/types.ts b/apps/sim/app/(landing)/integrations/data/types.ts index 7ba6483d6b8..bcdc0f732ac 100644 --- a/apps/sim/app/(landing)/integrations/data/types.ts +++ b/apps/sim/app/(landing)/integrations/data/types.ts @@ -19,6 +19,29 @@ export interface FAQItem { answer: string } +export interface IntegrationInstallStep { + title: string + body: string +} + +export interface IntegrationLandingContent { + /** + * Install walkthrough for OAuth apps whose connection lives behind sign-in. + * Provides the "Add to {app}" instructions that app marketplaces require + * when the install button sits behind a login. + */ + install?: { + heading: string + intro: string + steps: IntegrationInstallStep[] + } + /** Short data-handling summary shown next to a privacy-policy link. */ + privacy?: { + body: string + href: string + } +} + export interface Integration { type: string slug: string @@ -36,4 +59,5 @@ export interface Integration { category: string integrationTypes?: string[] tags?: string[] + landingContent?: IntegrationLandingContent } diff --git a/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts b/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts index d862fe277f1..55d1b90a958 100644 --- a/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts +++ b/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts @@ -1,6 +1,6 @@ import type { NextResponse } from 'next/server' -import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' export async function GET(): Promise { - return createMcpAuthorizationServerMetadataResponse() + return copilotMcpDeprecatedResponse() } diff --git a/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts b/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts index a419ebda324..55d1b90a958 100644 --- a/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts +++ b/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts @@ -1,6 +1,6 @@ import type { NextResponse } from 'next/server' -import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' export async function GET(): Promise { - return createMcpProtectedResourceMetadataResponse() + return copilotMcpDeprecatedResponse() } diff --git a/apps/sim/app/api/billing/route.ts b/apps/sim/app/api/billing/route.ts index 1b9411647e8..2cc338abd48 100644 --- a/apps/sim/app/api/billing/route.ts +++ b/apps/sim/app/api/billing/route.ts @@ -140,7 +140,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { members: rawBillingData.members.map((m) => ({ ...m, joinedAt: m.joinedAt.toISOString(), - lastActive: m.lastActive?.toISOString() || null, })), } diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index e9d19853c04..03bf505a4df 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -12,6 +12,7 @@ import { validatePII } from '@/lib/guardrails/validate_pii' import { validateRegex } from '@/lib/guardrails/validate_regex' import { assertPermissionsAllowed, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' @@ -161,7 +162,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError) { + if (err instanceof ProviderNotAllowedError || err instanceof ModelNotAllowedError) { return NextResponse.json({ success: true, output: { diff --git a/apps/sim/app/api/knowledge/utils.test.ts b/apps/sim/app/api/knowledge/utils.test.ts index 326bbee660f..b8297e4cf65 100644 --- a/apps/sim/app/api/knowledge/utils.test.ts +++ b/apps/sim/app/api/knowledge/utils.test.ts @@ -259,7 +259,12 @@ describe('Knowledge Utils', () => { {} ) - expect(dbOps.order).toEqual(['insert', 'updateDoc']) + // Embeddings are inserted first, then the document counter update. A + // usage_log billing insert (recordUsage) may trail after updateDoc and is + // irrelevant to this ordering invariant, so assert position rather than + // exact array equality. + expect(dbOps.order[0]).toBe('insert') + expect(dbOps.order.indexOf('updateDoc')).toBeGreaterThan(0) expect(dbOps.updatePayloads[0]).toMatchObject({ processingStatus: 'completed', diff --git a/apps/sim/app/api/logs/execution/[executionId]/route.ts b/apps/sim/app/api/logs/execution/[executionId]/route.ts index 71deb54267d..adab287bf99 100644 --- a/apps/sim/app/api/logs/execution/[executionId]/route.ts +++ b/apps/sim/app/api/logs/execution/[executionId]/route.ts @@ -13,6 +13,7 @@ import { executionIdParamsSchema } from '@/lib/api/contracts/logs' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' const logger = createLogger('LogsByExecutionIdAPI') @@ -39,13 +40,14 @@ export const GET = withRouteHandler( .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) @@ -119,7 +121,14 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 }) } - const executionData = workflowLog.executionData as WorkflowExecutionLog['executionData'] + const executionData = (await materializeExecutionData( + workflowLog.executionData as Record | null, + { + workspaceId: workflowLog.workspaceId, + workflowId: workflowLog.workflowId, + executionId: workflowLog.executionId, + } + )) as WorkflowExecutionLog['executionData'] const traceSpans = (executionData?.traceSpans as TraceSpan[]) || [] const childSnapshotIds = new Set() const collectSnapshotIds = (spans: TraceSpan[]) => { @@ -163,7 +172,7 @@ export const GET = withRouteHandler( startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, - cost: workflowLog.cost || null, + cost: workflowLog.costTotal != null ? { total: Number(workflowLog.costTotal) } : null, }, } diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 2c817411b68..766435eadd4 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -4,7 +4,9 @@ import { createLogger } from '@sim/logger' import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' @@ -41,7 +43,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, workflowName: sql`COALESCE(${workflow.name}, 'Deleted Workflow')`, } @@ -96,11 +98,29 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!rows.length) break - for (const r of rows as any[]) { + // Heavy execution data may live in object storage; materialize per + // row with bounded concurrency so a 1000-row page doesn't fan out + // into 1000 simultaneous reads. + const materialized = await mapWithConcurrency( + rows as any[], + MATERIALIZE_CONCURRENCY, + (r) => + materializeExecutionData(r.executionData as Record | null, { + workspaceId: params.workspaceId, + workflowId: r.workflowId, + executionId: r.executionId, + }) + ) + + for (let j = 0; j < rows.length; j++) { + const r = rows[j] as any + const ed = materialized[j] as Record + // A single malformed/unserializable row must not abort the whole CSV + // stream — derive the message/trace columns defensively and fall back + // to empty on error so the row's metadata still exports. let message = '' - let traces: any = null + let tracesJson = '' try { - const ed = (r as any).executionData if (ed) { if (ed.finalOutput) message = @@ -108,20 +128,25 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ? ed.finalOutput : JSON.stringify(ed.finalOutput) if (ed.message) message = ed.message - if (ed.traceSpans) traces = ed.traceSpans + if (ed.traceSpans) tracesJson = JSON.stringify(ed.traceSpans) } - } catch {} + } catch (rowError) { + logger.warn('Skipping unserializable execution data for export row', { + executionId: r.executionId, + error: rowError instanceof Error ? rowError.message : String(rowError), + }) + } const line = [ escapeCsv(r.startedAt?.toISOString?.() || r.startedAt), escapeCsv(r.level), escapeCsv(r.workflowName), escapeCsv(r.trigger), escapeCsv(r.totalDurationMs ?? ''), - escapeCsv(r.cost?.total ?? r.cost?.value?.total ?? ''), + escapeCsv(r.costTotal ?? ''), escapeCsv(r.workflowId ?? ''), escapeCsv(r.executionId ?? ''), escapeCsv(message), - escapeCsv(traces ? JSON.stringify(traces) : ''), + escapeCsv(tracesJson), ].join(',') controller.enqueue(encoder.encode(`${line}\n`)) } diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index 89f52048b72..548383c1035 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -31,6 +31,7 @@ import { listLogsContract, type WorkflowLogSummary } from '@/lib/api/contracts/l import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { jobCostTotal } from '@/lib/logs/fetch-log-detail' import { buildFilterConditions } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' @@ -81,7 +82,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { case 'duration': return sql`${workflowExecutionLogs.totalDurationMs}` case 'cost': - return sql`(${workflowExecutionLogs.cost}->>'total')::numeric` + // Indexed projection of the usage_log ledger (dollars); no live aggregation. + return sql`${workflowExecutionLogs.costTotal}` case 'status': return sql`${workflowExecutionLogs.status}` default: @@ -201,7 +203,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, workflowDescription: workflow.description, @@ -379,7 +381,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } : null, jobTitle: null, - cost: (log.cost as WorkflowLogSummary['cost']) ?? null, + // List cost is the cost_total projection (faithful ledger sum). Null until + // completion (running) or until the one-time legacy backfill populates it. + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, pauseSummary: { status: log.pausedStatus ?? null, total: totalPauseCount, @@ -405,7 +409,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { createdAt: log.startedAt.toISOString(), workflow: null, jobTitle: log.jobTitle ?? null, - cost: (log.cost as WorkflowLogSummary['cost']) ?? null, + cost: jobCostTotal(log.cost), pauseSummary: { status: null, total: 0, resumed: 0 }, hasPendingPause: false, } diff --git a/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts b/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts index 3f2976ff027..c54b72dfcb8 100644 --- a/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts +++ b/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts @@ -1,12 +1,4 @@ -import type { NextRequest, NextResponse } from 'next/server' -import { mcpOauthAuthorizationServerMetadataContract } from '@/lib/api/contracts/mcp-oauth' -import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' -export const GET = withRouteHandler(async (request: NextRequest): Promise => { - const parsed = await parseRequest(mcpOauthAuthorizationServerMetadataContract, request, {}) - if (!parsed.success) return parsed.response as NextResponse - - return createMcpAuthorizationServerMetadataResponse() -}) +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) diff --git a/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts b/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts index 1e17b126b31..c54b72dfcb8 100644 --- a/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts +++ b/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts @@ -1,12 +1,4 @@ -import type { NextRequest, NextResponse } from 'next/server' -import { mcpOauthProtectedResourceMetadataContract } from '@/lib/api/contracts/mcp-oauth' -import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' -export const GET = withRouteHandler(async (request: NextRequest): Promise => { - const parsed = await parseRequest(mcpOauthProtectedResourceMetadataContract, request, {}) - if (!parsed.success) return parsed.response as NextResponse - - return createMcpProtectedResourceMetadataResponse() -}) +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) diff --git a/apps/sim/app/api/mcp/copilot/route.test.ts b/apps/sim/app/api/mcp/copilot/route.test.ts new file mode 100644 index 00000000000..7db9860fbce --- /dev/null +++ b/apps/sim/app/api/mcp/copilot/route.test.ts @@ -0,0 +1,62 @@ +/** + * Tests for the deprecated Copilot MCP route + * + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { GET as authServerDiscoveryGET } from '@/app/api/mcp/copilot/.well-known/oauth-authorization-server/route' +import { GET as protectedResourceDiscoveryGET } from '@/app/api/mcp/copilot/.well-known/oauth-protected-resource/route' +import { DELETE, GET, POST } from '@/app/api/mcp/copilot/route' + +const URL = 'http://localhost:3000/api/mcp/copilot' + +describe('Deprecated Copilot MCP route', () => { + it('GET returns 410', async () => { + const response = await GET(new NextRequest(URL)) + expect(response.status).toBe(410) + }) + + it('POST returns 410 with a JSON-RPC error envelope', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + + const body = (await response.json()) as { jsonrpc?: string; error?: { message?: string } } + expect(body.jsonrpc).toBe('2.0') + expect(body.error?.message).toContain('deprecated') + }) + + it('POST still returns 410 when an x-api-key header is present', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': 'sk-sim-copilot-test' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + }) + + it('DELETE returns 410', async () => { + const response = await DELETE(new NextRequest(URL, { method: 'DELETE' })) + expect(response.status).toBe(410) + }) + + it('copilot OAuth authorization-server discovery returns 410', async () => { + const response = await authServerDiscoveryGET( + new NextRequest(`${URL}/.well-known/oauth-authorization-server`) + ) + expect(response.status).toBe(410) + }) + + it('copilot OAuth protected-resource discovery returns 410', async () => { + const response = await protectedResourceDiscoveryGET( + new NextRequest(`${URL}/.well-known/oauth-protected-resource`) + ) + expect(response.status).toBe(410) + }) +}) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 2bda0f3670f..6ec1475d2ef 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -1,740 +1,13 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js' -import { - CallToolRequestSchema, - type CallToolResult, - ErrorCode, - type JSONRPCError, - ListToolsRequestSchema, - type ListToolsResult, - McpError, - type RequestId, -} from '@modelcontextprotocol/sdk/types.js' -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' -import { type NextRequest, NextResponse } from 'next/server' -import { mcpRequestBodySchema, mcpToolCallParamsSchema } from '@/lib/api/contracts/mcp' -import { validateOAuthAccessToken } from '@/lib/auth/oauth-token' -import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' -import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' -import { ORCHESTRATION_TIMEOUT_MS, SIM_AGENT_API_URL } from '@/lib/copilot/constants' -import { createRequestId } from '@/lib/copilot/request/http' -import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' -import { orchestrateSubagentStream } from '@/lib/copilot/request/subagent' -import { ensureHandlersRegistered, executeTool } from '@/lib/copilot/tool-executor' -import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' -import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' -import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions' -import { env } from '@/lib/core/config/env' -import { RateLimiter } from '@/lib/core/rate-limiter' -import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' - -const logger = createLogger('CopilotMcpAPI') -const mcpRateLimiter = new RateLimiter() -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' +import { + copilotMcpDeprecatedJsonRpcResponse, + copilotMcpDeprecatedResponse, +} from '@/lib/mcp/copilot-deprecated' export const dynamic = 'force-dynamic' -export const runtime = 'nodejs' -export const maxDuration = 3600 - -interface CopilotKeyAuthResult { - success: boolean - userId?: string - error?: string -} - -/** - * Validates a copilot API key by forwarding it to the Go copilot service's - * `/api/validate-key` endpoint. Returns the associated userId on success. - */ -async function authenticateCopilotApiKey(apiKey: string): Promise { - try { - const internalSecret = env.INTERNAL_API_SECRET - if (!internalSecret) { - logger.error('INTERNAL_API_SECRET not configured') - return { success: false, error: 'Server configuration error' } - } - - const { fetchGo } = await import('@/lib/copilot/request/go/fetch') - const res = await fetchGo(`${SIM_AGENT_API_URL}/api/validate-key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': internalSecret, - }, - body: JSON.stringify({ targetApiKey: apiKey }), - signal: AbortSignal.timeout(10_000), - spanName: 'sim → go /api/validate-key (mcp)', - operation: 'mcp_validate_key', - }) - - if (!res.ok) { - const body = await res.json().catch(() => null) - const upstream = (body as Record)?.message - const status = res.status - - if (status === 401 || status === 403) { - return { - success: false, - error: `Invalid Copilot API key. Generate a new key in Settings → Copilot and set it in the x-api-key header.`, - } - } - if (status === 402) { - return { - success: false, - error: `Usage limit exceeded for this Copilot API key. Upgrade your plan or wait for your quota to reset.`, - } - } - - return { - success: false, - error: String(upstream ?? 'Copilot API key validation failed'), - } - } - - const data = (await res.json()) as { ok?: boolean; userId?: string } - if (!data.ok || !data.userId) { - return { - success: false, - error: 'Invalid Copilot API key. Generate a new key in Settings → Copilot.', - } - } - - return { success: true, userId: data.userId } - } catch (error) { - logger.error('Copilot API key validation failed', { error }) - return { - success: false, - error: - 'Could not validate Copilot API key — the authentication service is temporarily unreachable. This is NOT a problem with the API key itself; please retry shortly.', - } - } -} - -/** - * MCP Server instructions that guide LLMs on how to use the Sim copilot tools. - * This is included in the initialize response to help external LLMs understand - * the workflow lifecycle and best practices. - */ -const MCP_SERVER_INSTRUCTIONS = ` -## Sim Workflow Copilot - -Sim is a workflow automation platform. Workflows are visual pipelines of connected blocks (Agent, Function, Condition, API, integrations, etc.). The Agent block is the core — an LLM with tools, memory, structured output, and knowledge bases. - -### Workflow Lifecycle (Happy Path) - -1. \`list_workspaces\` → know where to work -2. \`create_workflow(name, workspaceId)\` → get a workflowId -3. \`sim_workflow(request, workflowId)\` → plan and build in one pass -4. \`sim_test(request, workflowId)\` → verify it works -5. \`sim_deploy("deploy as api", workflowId)\` → make it accessible externally (optional) - -### Working with Existing Workflows - -When the user refers to a workflow by name or description ("the email one", "my Slack bot"): -1. Use \`sim_discovery\` to find it by functionality -2. Or use \`list_workflows\` and match by name -3. Then pass the workflowId to other tools - -### Organization - -- \`rename_workflow\` — rename a workflow -- \`move_workflow\` — move a workflow into a folder (or back to root by clearing the folder id) -- \`move_folder\` — nest a folder inside another (or move it back to root by clearing the parent id) -- \`create_folder(name, parentId)\` — create nested folder hierarchies - -### Key Rules - -- You can test workflows immediately after building — deployment is only needed for external access (API, chat, MCP). -- Tools that operate on a specific workflow such as \`sim_workflow\`, \`sim_test\`, \`sim_deploy\`, and workflow-scoped \`sim_info\` requests require \`workflowId\`. -- If the user reports errors, route through \`sim_workflow\` and ask it to reproduce, inspect logs, and fix the issue end to end. -- Variable syntax: \`\` for block outputs, \`{{ENV_VAR}}\` for env vars. -` - -type HeaderMap = Record - -function createError(id: RequestId, code: ErrorCode | number, message: string): JSONRPCError { - return { - jsonrpc: '2.0', - id, - error: { code, message }, - } -} - -function readHeader(headers: HeaderMap | undefined, name: string): string | undefined { - if (!headers) return undefined - const value = headers[name.toLowerCase()] - if (Array.isArray(value)) { - return value[0] - } - return value -} - -function buildMcpServer(abortSignal?: AbortSignal): Server { - const server = new Server( - { - name: 'sim-copilot', - version: '1.0.0', - }, - { - capabilities: { tools: {} }, - instructions: MCP_SERVER_INSTRUCTIONS, - } - ) - - server.setRequestHandler(ListToolsRequestSchema, async () => { - const directTools = DIRECT_TOOL_DEFS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - ...(tool.annotations && { annotations: tool.annotations }), - })) - - const subagentTools = SUBAGENT_TOOL_DEFS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - ...(tool.annotations && { annotations: tool.annotations }), - })) - - const result: ListToolsResult = { - tools: [...directTools, ...subagentTools], - } - - return result - }) - - server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { - const headers = (extra.requestInfo?.headers || {}) as HeaderMap - const apiKeyHeader = readHeader(headers, 'x-api-key') - const authorizationHeader = readHeader(headers, 'authorization') - - let authResult: CopilotKeyAuthResult = { success: false } - - if (authorizationHeader?.startsWith('Bearer ')) { - const token = authorizationHeader.slice(7) - const oauthResult = await validateOAuthAccessToken(token) - if (oauthResult.success && oauthResult.userId) { - if (!oauthResult.scopes?.includes('mcp:tools')) { - return { - content: [ - { - type: 'text' as const, - text: 'AUTHENTICATION ERROR: OAuth token is missing the required "mcp:tools" scope. Re-authorize with the correct scopes.', - }, - ], - isError: true, - } - } - authResult = { success: true, userId: oauthResult.userId } - } else { - return { - content: [ - { - type: 'text' as const, - text: `AUTHENTICATION ERROR: ${oauthResult.error ?? 'Invalid OAuth access token'} Do NOT retry — re-authorize via OAuth.`, - }, - ], - isError: true, - } - } - } else if (apiKeyHeader) { - authResult = await authenticateCopilotApiKey(apiKeyHeader) - } - - if (!authResult.success || !authResult.userId) { - const errorMsg = apiKeyHeader - ? `AUTHENTICATION ERROR: ${authResult.error} Do NOT retry — this will fail until the user fixes their Copilot API key.` - : 'AUTHENTICATION ERROR: No authentication provided. Provide a Bearer token (OAuth 2.1) or an x-api-key header. Generate a Copilot API key in Settings → Copilot.' - logger.warn('MCP copilot auth failed', { method: request.method }) - return { - content: [ - { - type: 'text' as const, - text: errorMsg, - }, - ], - isError: true, - } - } - - const rateLimitResult = await mcpRateLimiter.checkRateLimitWithSubscription( - authResult.userId, - await getHighestPrioritySubscription(authResult.userId), - 'api-endpoint', - false - ) - - if (!rateLimitResult.allowed) { - return { - content: [ - { - type: 'text' as const, - text: `RATE LIMIT: Too many requests. Please wait and retry after ${rateLimitResult.resetAt.toISOString()}.`, - }, - ], - isError: true, - } - } - - const paramsValidation = mcpToolCallParamsSchema.safeParse(request.params) - if (!paramsValidation.success) { - throw new McpError(ErrorCode.InvalidParams, 'Tool name required') - } - const params = paramsValidation.data - - const result = await handleToolsCall( - { - name: params.name, - arguments: params.arguments, - }, - authResult.userId, - abortSignal - ) - - return result - }) - - return server -} - -async function handleMcpRequestWithSdk( - request: NextRequest, - parsedBody: unknown -): Promise { - const server = buildMcpServer(request.signal) - const transport = new WebStandardStreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, - }) - - await server.connect(transport) - - try { - return await transport.handleRequest(request, { parsedBody }) - } finally { - await server.close().catch(() => {}) - await transport.close().catch(() => {}) - } -} - -export const GET = withRouteHandler(async () => { - // Return 405 to signal that server-initiated SSE notifications are not - // supported. Without this, clients like mcp-remote will repeatedly - // reconnect trying to open an SSE stream, flooding the logs with GETs. - return new NextResponse(null, { status: 405 }) -}) - -export const POST = withRouteHandler(async (request: NextRequest) => { - const hasAuth = request.headers.has('authorization') || request.headers.has('x-api-key') - - if (!hasAuth) { - const origin = getBaseUrl().replace(/\/$/, '') - const resourceMetadataUrl = `${origin}/.well-known/oauth-protected-resource/api/mcp/copilot` - return new NextResponse(JSON.stringify({ error: 'unauthorized' }), { - status: 401, - headers: { - 'WWW-Authenticate': `Bearer resource_metadata="${resourceMetadataUrl}", scope="mcp:tools"`, - 'Content-Type': 'application/json', - }, - }) - } - - try { - let parsedBody: unknown - - try { - parsedBody = await request.json() - } catch { - return NextResponse.json(createError(0, ErrorCode.ParseError, 'Invalid JSON body'), { - status: 400, - }) - } - - const bodyValidation = mcpRequestBodySchema.safeParse(parsedBody) - if (!bodyValidation.success) { - return NextResponse.json( - createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'), - { - status: 400, - } - ) - } - - return await handleMcpRequestWithSdk(request, bodyValidation.data) - } catch (error) { - if (request.signal.aborted || (error as Error)?.name === 'AbortError') { - return NextResponse.json( - createError(0, ErrorCode.ConnectionClosed, 'Client cancelled request'), - { status: 499 } - ) - } - - logger.error('Error handling MCP request', { error }) - return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), { - status: 500, - }) - } -}) - -export const DELETE = withRouteHandler(async (request: NextRequest) => { - void request - return NextResponse.json(createError(0, -32000, 'Method not allowed.'), { status: 405 }) -}) - -async function handleToolsCall( - params: { name: string; arguments?: Record }, - userId: string, - abortSignal?: AbortSignal -): Promise { - const args = params.arguments || {} - - const directTool = DIRECT_TOOL_DEFS.find((tool) => tool.name === params.name) - if (directTool) { - return handleDirectToolCall(directTool, args, userId) - } - - const subagentTool = SUBAGENT_TOOL_DEFS.find((tool) => tool.name === params.name) - if (subagentTool) { - return handleSubagentToolCall(subagentTool, args, userId, abortSignal) - } - - throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${params.name}`) -} - -async function handleDirectToolCall( - toolDef: (typeof DIRECT_TOOL_DEFS)[number], - args: Record, - userId: string -): Promise { - try { - const rawWorkflowId = (args.workflowId as string) || '' - let resolvedWorkspaceId: string | undefined - if (rawWorkflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: rawWorkflowId, - userId, - action: 'read', - }) - if (!authorization.allowed) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { success: false, error: 'Workflow not found or access denied' }, - null, - 2 - ), - }, - ], - isError: true, - } - } - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - } - const execContext = await prepareExecutionContext( - userId, - rawWorkflowId, - (args.chatId as string) || undefined, - { workspaceId: resolvedWorkspaceId } - ) - - const toolCall = { - id: generateId(), - name: toolDef.toolId, - status: 'pending' as const, - params: args as Record, - startTime: Date.now(), - } - - ensureHandlersRegistered() - const result = await executeTool(toolCall.name, toolCall.params || {}, execContext) - - return { - content: [ - { - type: 'text', - text: JSON.stringify(result.output ?? result, null, 2), - }, - ], - isError: !result.success, - } - } catch (error) { - logger.error('Direct tool execution failed', { tool: toolDef.name, error }) - return { - content: [ - { - type: 'text', - text: `Tool execution failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} - -/** - * Build mode uses the main /api/mcp orchestrator instead of /api/subagent/workflow. - * The main agent still delegates workflow work to the workflow subagent inside Go; - * this helper simply uses the full headless lifecycle so build requests behave like - * the primary MCP chat flow. - */ -async function handleBuildToolCall( - args: Record, - userId: string, - abortSignal?: AbortSignal -): Promise { - try { - const requestText = (args.request as string) || JSON.stringify(args) - const workflowId = args.workflowId as string | undefined - let resolvedWorkflowName: string | undefined - let resolvedWorkspaceId: string | undefined - - const resolved = workflowId - ? await (async () => { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId, - userId, - action: 'read', - }) - resolvedWorkflowName = authorization.workflow?.name || undefined - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - return authorization.allowed - ? { - status: 'resolved' as const, - workflowId, - workflowName: resolvedWorkflowName, - } - : { - status: 'not_found' as const, - message: 'workflowId is required for build. Call create_workflow first.', - } - })() - : await resolveWorkflowIdForUser(userId) - - if (resolved.status === 'resolved') { - resolvedWorkflowName ||= resolved.workflowName - } - - if (!resolved || resolved.status !== 'resolved') { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - success: false, - error: - resolved?.message ?? - 'workflowId is required for build. Call create_workflow first.', - }, - null, - 2 - ), - }, - ], - isError: true, - } - } - - const chatId = generateId() - const executionContext = await prepareExecutionContext(userId, resolved.workflowId, chatId, { - workspaceId: resolvedWorkspaceId, - }) - resolvedWorkspaceId = executionContext.workspaceId - let workspaceContext: string | undefined - if (resolvedWorkspaceId) { - try { - workspaceContext = await generateWorkspaceContext(resolvedWorkspaceId, userId) - } catch (error) { - logger.warn('Failed to generate workspace context for build tool call', { - workflowId: resolved.workflowId, - workspaceId: resolvedWorkspaceId, - error: toError(error).message, - }) - } - } - - const requestPayload = { - message: requestText, - workflowId: resolved.workflowId, - ...(resolvedWorkflowName ? { workflowName: resolvedWorkflowName } : {}), - ...(resolvedWorkspaceId ? { workspaceId: resolvedWorkspaceId } : {}), - ...(workspaceContext ? { workspaceContext } : {}), - userId, - model: DEFAULT_COPILOT_MODEL, - mode: 'agent', - commands: ['fast'], - messageId: generateId(), - chatId, - } - - const result = await runHeadlessCopilotLifecycle(requestPayload, { - userId, - workflowId: resolved.workflowId, - workspaceId: resolvedWorkspaceId, - chatId, - goRoute: '/api/mcp', - executionContext, - autoExecuteTools: true, - timeout: ORCHESTRATION_TIMEOUT_MS, - interactive: false, - abortSignal, - }) - - const responseData = { - success: result.success, - content: result.content, - toolCalls: result.toolCalls, - error: result.error, - } - - return { - content: [{ type: 'text', text: JSON.stringify(responseData, null, 2) }], - isError: !result.success, - } - } catch (error) { - logger.error('Build tool call failed', { error }) - return { - content: [ - { - type: 'text', - text: `Build failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} - -async function handleSubagentToolCall( - toolDef: (typeof SUBAGENT_TOOL_DEFS)[number], - args: Record, - userId: string, - abortSignal?: AbortSignal -): Promise { - if (toolDef.agentId === 'workflow') { - return handleBuildToolCall(args, userId, abortSignal) - } - - try { - const requestText = - (args.request as string) || - (args.message as string) || - (args.error as string) || - JSON.stringify(args) - const simRequestId = createRequestId() - - const context = (args.context as Record) || {} - if (args.plan && !context.plan) { - context.plan = args.plan - } - - // Authorize user-supplied workflowId / workspaceId before forwarding downstream - const rawWorkflowId = args.workflowId as string | undefined - const rawWorkspaceId = args.workspaceId as string | undefined - let resolvedWorkflowId: string | undefined - let resolvedWorkspaceId: string | undefined - - if (rawWorkflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: rawWorkflowId, - userId, - action: 'read', - }) - if (!authorization.allowed) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { success: false, error: 'Workflow not found or access denied' }, - null, - 2 - ), - }, - ], - isError: true, - } - } - resolvedWorkflowId = rawWorkflowId - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - } else if (rawWorkspaceId) { - await ensureWorkspaceAccess(rawWorkspaceId, userId, 'read') - resolvedWorkspaceId = rawWorkspaceId - } - - const result = await orchestrateSubagentStream( - toolDef.agentId, - { - message: requestText, - workflowId: resolvedWorkflowId, - workspaceId: resolvedWorkspaceId, - context, - model: DEFAULT_COPILOT_MODEL, - headless: true, - source: 'mcp', - }, - { - userId, - workflowId: resolvedWorkflowId, - workspaceId: resolvedWorkspaceId, - simRequestId, - abortSignal, - } - ) - - let responseData: unknown - if (result.structuredResult) { - responseData = { - success: result.structuredResult.success ?? result.success, - type: result.structuredResult.type, - summary: result.structuredResult.summary, - data: result.structuredResult.data, - } - } else if (result.error) { - responseData = { - success: false, - error: result.error, - errors: result.errors, - } - } else { - responseData = { - success: result.success, - content: result.content, - } - } +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) - return { - content: [ - { - type: 'text', - text: JSON.stringify(responseData, null, 2), - }, - ], - isError: !result.success, - } - } catch (error) { - logger.error('Subagent tool call failed', { - tool: toolDef.name, - agentId: toolDef.agentId, - error, - }) +export const POST = withRouteHandler(async () => copilotMcpDeprecatedJsonRpcResponse()) - return { - content: [ - { - type: 'text', - text: `Subagent call failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} +export const DELETE = withRouteHandler(async () => copilotMcpDeprecatedJsonRpcResponse()) diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index 89d2cc81cb7..ed6bcf4d5b8 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -11,6 +11,7 @@ import { import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { getUserUsageData } from '@/lib/billing/core/usage' import { removeExternalUserFromOrganizationWorkspaces, @@ -101,10 +102,25 @@ export const GET = withRouteHandler( const computed = await getUserUsageData(memberId) if (usageData.length > 0) { + // currentPeriodCost is only a baseline; add this member's attributed + // usage_log for the period. (getUserUsageData returns the org POOL for + // org-scoped members, so it can't supply the per-member figure.) + const memberLedger = + ( + await getOrgMemberLedgerByUser( + organizationId, + computed.billingPeriodStart && computed.billingPeriodEnd + ? { start: computed.billingPeriodStart, end: computed.billingPeriodEnd } + : null + ) + ).get(memberId) ?? 0 memberData = { ...memberData, usage: { ...usageData[0], + currentPeriodCost: ( + Number(usageData[0].currentPeriodCost ?? 0) + memberLedger + ).toString(), billingPeriodStart: computed.billingPeriodStart, billingPeriodEnd: computed.billingPeriodEnd, }, diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 48a356215cf..e412a7e635f 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -17,6 +17,7 @@ import { } from '@/lib/api/contracts/organization' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -139,8 +140,21 @@ export const GET = withRouteHandler( const billingPeriodStart = orgSub?.periodStart ?? null const billingPeriodEnd = orgSub?.periodEnd ?? null + // currentPeriodCost is only a baseline; add each member's attributed + // usage_log for the period (batched, one query) so the roster shows real + // usage rather than the frozen baseline. + const usageByUser = await getOrgMemberLedgerByUser( + organizationId, + billingPeriodStart && billingPeriodEnd + ? { start: billingPeriodStart, end: billingPeriodEnd } + : null + ) + const membersWithUsage = base.map((row) => ({ ...row, + currentPeriodCost: ( + Number(row.currentPeriodCost ?? 0) + (usageByUser.get(row.userId) ?? 0) + ).toString(), billingPeriodStart, billingPeriodEnd, })) diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index f0bfc2b4a45..bc5e344772b 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -19,6 +19,7 @@ import { import { assertPermissionsAllowed, IntegrationNotAllowedError, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' import type { StreamingExecution } from '@/executor/types' @@ -132,7 +133,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError || err instanceof IntegrationNotAllowedError) { + if ( + err instanceof ProviderNotAllowedError || + err instanceof ModelNotAllowedError || + err instanceof IntegrationNotAllowedError + ) { return NextResponse.json({ error: err.message }, { status: 403 }) } throw err diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 4dadf2cf93e..7a33e113133 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -35,6 +35,7 @@ import { adminV1UpdateOrganizationMemberContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -86,7 +87,6 @@ export const GET = withRouteHandler( userEmail: user.email, currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, billingBlocked: userStats.billingBlocked, }) .from(member) @@ -99,6 +99,10 @@ export const GET = withRouteHandler( return notFoundResponse('Member') } + // currentPeriodCost is only a baseline; add this member's attributed + // usage_log for the org's period so admin shows real current usage. + const ledgerByUser = await getOrgMemberLedgerByUser(organizationId) + const data: AdminMemberDetail = { id: memberData.id, userId: memberData.userId, @@ -107,9 +111,10 @@ export const GET = withRouteHandler( createdAt: memberData.createdAt.toISOString(), userName: memberData.userName, userEmail: memberData.userEmail, - currentPeriodCost: memberData.currentPeriodCost ?? '0', + currentPeriodCost: ( + Number(memberData.currentPeriodCost ?? 0) + (ledgerByUser.get(memberData.userId) ?? 0) + ).toString(), currentUsageLimit: memberData.currentUsageLimit, - lastActive: memberData.lastActive?.toISOString() ?? null, billingBlocked: memberData.billingBlocked ?? false, } diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index 0c5b50fefd6..5c4466d182d 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -37,6 +37,7 @@ import { adminV1ListOrganizationMembersContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { addUserToOrganization } from '@/lib/billing/organizations/membership' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -96,7 +97,6 @@ export const GET = withRouteHandler( userEmail: user.email, currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, billingBlocked: userStats.billingBlocked, }) .from(member) @@ -109,6 +109,11 @@ export const GET = withRouteHandler( ]) const total = countResult[0].count + + // currentPeriodCost is only a baseline; add each member's attributed + // usage_log for the org's period so admin shows real current usage. + const usageByUser = await getOrgMemberLedgerByUser(organizationId) + const data: AdminMemberDetail[] = membersData.map((m) => ({ id: m.id, userId: m.userId, @@ -117,9 +122,10 @@ export const GET = withRouteHandler( createdAt: m.createdAt.toISOString(), userName: m.userName, userEmail: m.userEmail, - currentPeriodCost: m.currentPeriodCost ?? '0', + currentPeriodCost: ( + Number(m.currentPeriodCost ?? 0) + (usageByUser.get(m.userId) ?? 0) + ).toString(), currentUsageLimit: m.currentUsageLimit, - lastActive: m.lastActive?.toISOString() ?? null, billingBlocked: m.billingBlocked ?? false, })) diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts index 6c015190408..921131e2409 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts @@ -51,16 +51,6 @@ export const GET = withRouteHandler( subscriptionPlan: analytics.subscriptionPlan, canAddSeats: analytics.canAddSeats, utilizationRate: analytics.utilizationRate, - activeMembers: analytics.activeMembers, - inactiveMembers: analytics.inactiveMembers, - memberActivity: analytics.memberActivity.map((m) => ({ - userId: m.userId, - userName: m.userName, - userEmail: m.userEmail, - role: m.role, - joinedAt: m.joinedAt.toISOString(), - lastActive: m.lastActive?.toISOString() ?? null, - })), } logger.info(`Admin API: Retrieved seat analytics for organization ${organizationId}`) diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts index 3cdbfc46d27..eb3ce167e0a 100644 --- a/apps/sim/app/api/v1/admin/types.ts +++ b/apps/sim/app/api/v1/admin/types.ts @@ -543,7 +543,6 @@ export interface AdminMemberDetail extends AdminMember { // Billing/usage info from userStats currentPeriodCost: string currentUsageLimit: string | null - lastActive: string | null billingBlocked: boolean } @@ -574,28 +573,15 @@ interface AdminUserBilling { userEmail: string stripeCustomerId: string | null // Usage stats - totalManualExecutions: number - totalApiCalls: number - totalWebhookTriggers: number - totalScheduledExecutions: number - totalChatExecutions: number - totalMcpExecutions: number - totalA2aExecutions: number - totalTokensUsed: number - totalCost: string currentUsageLimit: string | null currentPeriodCost: string lastPeriodCost: string | null billedOverageThisPeriod: string storageUsedBytes: number - lastActive: string | null billingBlocked: boolean - // Copilot usage - totalCopilotCost: string + // Copilot usage (active per-period baselines) currentPeriodCopilotCost: string lastPeriodCopilotCost: string | null - totalCopilotTokens: number - totalCopilotCalls: number } export interface AdminUserBillingWithSubscription extends AdminUserBilling { @@ -643,16 +629,6 @@ export interface AdminSeatAnalytics { subscriptionPlan: string canAddSeats: boolean utilizationRate: number - activeMembers: number - inactiveMembers: number - memberActivity: Array<{ - userId: string - userName: string - userEmail: string - role: string - joinedAt: string - lastActive: string | null - }> } export interface AdminDeploymentVersion { diff --git a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts index 7c080a4e6e5..85a5f69b63a 100644 --- a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts @@ -29,6 +29,7 @@ import { } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import { getUserUsageData } from '@/lib/billing/core/usage' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' @@ -78,6 +79,11 @@ export const GET = withRouteHandler( const [stats] = await db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1) + // currentPeriodCost is now only a baseline; canonical current-period usage + // (baseline + attributed usage_log, refresh-adjusted) comes from the same + // helper users see, so admin reflects real usage instead of a stale 0. + const usage = await getUserUsageData(userId) + const memberOrgs = await db .select({ organizationId: member.organizationId, @@ -107,27 +113,14 @@ export const GET = withRouteHandler( userName: userData.name, userEmail: userData.email, stripeCustomerId: userData.stripeCustomerId, - totalManualExecutions: stats?.totalManualExecutions ?? 0, - totalApiCalls: stats?.totalApiCalls ?? 0, - totalWebhookTriggers: stats?.totalWebhookTriggers ?? 0, - totalScheduledExecutions: stats?.totalScheduledExecutions ?? 0, - totalChatExecutions: stats?.totalChatExecutions ?? 0, - totalMcpExecutions: stats?.totalMcpExecutions ?? 0, - totalA2aExecutions: stats?.totalA2aExecutions ?? 0, - totalTokensUsed: stats?.totalTokensUsed ?? 0, - totalCost: stats?.totalCost ?? '0', currentUsageLimit: stats?.currentUsageLimit ?? null, - currentPeriodCost: stats?.currentPeriodCost ?? '0', + currentPeriodCost: usage.currentUsage.toString(), lastPeriodCost: stats?.lastPeriodCost ?? null, billedOverageThisPeriod: stats?.billedOverageThisPeriod ?? '0', storageUsedBytes: stats?.storageUsedBytes ?? 0, - lastActive: stats?.lastActive?.toISOString() ?? null, billingBlocked: stats?.billingBlocked ?? false, - totalCopilotCost: stats?.totalCopilotCost ?? '0', currentPeriodCopilotCost: stats?.currentPeriodCopilotCost ?? '0', lastPeriodCopilotCost: stats?.lastPeriodCopilotCost ?? null, - totalCopilotTokens: stats?.totalCopilotTokens ?? 0, - totalCopilotCalls: stats?.totalCopilotCalls ?? 0, subscriptions: subscriptions.map(toAdminSubscription), organizationMemberships: memberOrgs.map((m) => ({ organizationId: m.organizationId, diff --git a/apps/sim/app/api/v1/copilot/chat/route.test.ts b/apps/sim/app/api/v1/copilot/chat/route.test.ts new file mode 100644 index 00000000000..fef0a7eec88 --- /dev/null +++ b/apps/sim/app/api/v1/copilot/chat/route.test.ts @@ -0,0 +1,26 @@ +/** + * Tests for the deprecated v1 copilot chat API route + * + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { POST } from '@/app/api/v1/copilot/chat/route' + +const URL = 'http://localhost:3000/api/v1/copilot/chat' + +describe('Deprecated v1 copilot chat route', () => { + it('POST returns 410 with a success:false error body', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': 'sk-test' }, + body: JSON.stringify({ message: 'hello' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + + const body = (await response.json()) as { success?: boolean; error?: string } + expect(body.success).toBe(false) + expect(body.error).toContain('deprecated') + }) +}) diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index 6eaa1424b77..ac3759d96fc 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -1,151 +1,18 @@ -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { type NextRequest, NextResponse } from 'next/server' -import { v1CopilotChatContract } from '@/lib/api/contracts/v1/copilot' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' +import { NextResponse } from 'next/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' -import { authenticateRequest } from '@/app/api/v1/middleware' - -export const maxDuration = 3600 - -const logger = createLogger('CopilotHeadlessAPI') -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' /** * POST /api/v1/copilot/chat - * Headless copilot endpoint for server-side orchestration. * - * workflowId is optional - if not provided: - * - If workflowName is provided, finds that workflow - * - If exactly one workflow is available, uses that workflow as context - * - Otherwise requires workflowId or workflowName to disambiguate + * Deprecated: the v1 headless copilot chat API has been removed. The endpoint + * returns 410 Gone for all callers. */ -export const POST = withRouteHandler(async (req: NextRequest) => { - let messageId: string | undefined - const authorized = await authenticateRequest(req, 'copilot-chat') - if (authorized instanceof NextResponse) { - return authorized - } - const { userId, rateLimit } = authorized - const auth = { - authenticated: true as const, - userId, - keyType: rateLimit.keyType, - workspaceId: rateLimit.workspaceId, - } - - try { - const parsedRequest = await parseRequest( - v1CopilotChatContract, - req, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request'), - details: error.issues, - }, - { status: 400 } - ), - invalidJsonResponse: () => - NextResponse.json({ success: false, error: 'Invalid request' }, { status: 400 }), - } - ) - if (!parsedRequest.success) return parsedRequest.response - - const parsed = parsedRequest.data.body - const selectedModel = parsed.model || DEFAULT_COPILOT_MODEL - - // Resolve workflow ID - const resolved = await resolveWorkflowIdForUser( - auth.userId, - parsed.workflowId, - parsed.workflowName, - auth.keyType === 'workspace' ? auth.workspaceId : undefined - ) - if (resolved.status !== 'resolved') { - return NextResponse.json( - { - success: false, - error: resolved.message, - }, - { status: 400 } - ) - } - - if (auth.keyType === 'workspace' && auth.workspaceId) { - const workflow = await getWorkflowById(resolved.workflowId) - if (!workflow?.workspaceId || workflow.workspaceId !== auth.workspaceId) { - return NextResponse.json( - { success: false, error: 'API key is not authorized for this workspace' }, - { status: 403 } - ) - } - } - - // Transform mode to transport mode (same as client API) - // build and agent both map to 'agent' on the backend - const effectiveMode = parsed.mode === 'agent' ? 'build' : parsed.mode - const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode - - // Always generate a chatId - required for artifacts system to work with subagents - const chatId = parsed.chatId || generateId() - - messageId = generateId() - logger.info( - messageId - ? `Received headless copilot chat start request [messageId:${messageId}]` - : 'Received headless copilot chat start request', - { - workflowId: resolved.workflowId, - workflowName: parsed.workflowName, - chatId, - mode: transportMode, - autoExecuteTools: parsed.autoExecuteTools, - timeout: parsed.timeout, - } - ) - const requestPayload = { - message: parsed.message, - workflowId: resolved.workflowId, - userId: auth.userId, - model: selectedModel, - mode: transportMode, - messageId, - chatId, - } - - const result = await runHeadlessCopilotLifecycle(requestPayload, { - userId: auth.userId, - workflowId: resolved.workflowId, - chatId, - goRoute: '/api/mcp', - autoExecuteTools: parsed.autoExecuteTools, - timeout: parsed.timeout, - interactive: false, - }) - - return NextResponse.json({ - success: result.success, - content: result.content, - toolCalls: result.toolCalls, - chatId: result.chatId || chatId, - error: result.error, - }) - } catch (error) { - logger.error( - messageId - ? `Headless copilot request failed [messageId:${messageId}]` - : 'Headless copilot request failed', - { - error: toError(error).message, - } - ) - return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 }) - } -}) +export const POST = withRouteHandler(async () => + NextResponse.json( + { + success: false, + error: 'The v1 copilot chat API has been deprecated and is no longer available.', + }, + { status: 410, headers: { 'Cache-Control': 'no-store' } } + ) +) diff --git a/apps/sim/app/api/v1/logs/[id]/route.ts b/apps/sim/app/api/v1/logs/[id]/route.ts index c32acfd444c..9858a1b4e24 100644 --- a/apps/sim/app/api/v1/logs/[id]/route.ts +++ b/apps/sim/app/api/v1/logs/[id]/route.ts @@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { v1GetLogContract } from '@/lib/api/contracts/v1/logs' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { checkRateLimit, @@ -50,7 +51,7 @@ export const GET = withRouteHandler( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, @@ -101,8 +102,15 @@ export const GET = withRouteHandler( totalDurationMs: log.totalDurationMs, files: log.files || undefined, workflow: workflowSummary, - executionData: log.executionData as any, - cost: log.cost as any, + executionData: (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as any, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, createdAt: log.createdAt.toISOString(), } diff --git a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts index e7503ecb071..eefad39bb80 100644 --- a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts +++ b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts @@ -70,7 +70,9 @@ export const GET = withRouteHandler( startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, - cost: workflowLog.cost || null, + // Sourced from the cost_total projection of the usage_log ledger + // (the deprecated cost jsonb column was dropped). + cost: workflowLog.costTotal != null ? { total: Number(workflowLog.costTotal) } : null, }, } diff --git a/apps/sim/app/api/v1/logs/filters.ts b/apps/sim/app/api/v1/logs/filters.ts index 4f9bdbdfc6c..0e409e4d53f 100644 --- a/apps/sim/app/api/v1/logs/filters.ts +++ b/apps/sim/app/api/v1/logs/filters.ts @@ -84,18 +84,19 @@ export function buildLogFilters(filters: LogFilters): SQL { conditions.push(lte(workflowExecutionLogs.totalDurationMs, filters.maxDurationMs)) } - // Cost filters + // Cost filters — indexed projection of the usage_log ledger (dollars). if (filters.minCost !== undefined) { - conditions.push(sql`(${workflowExecutionLogs.cost}->>'total')::numeric >= ${filters.minCost}`) + conditions.push(sql`${workflowExecutionLogs.costTotal} >= ${filters.minCost}`) } if (filters.maxCost !== undefined) { - conditions.push(sql`(${workflowExecutionLogs.cost}->>'total')::numeric <= ${filters.maxCost}`) + conditions.push(sql`${workflowExecutionLogs.costTotal} <= ${filters.maxCost}`) } - // Model filter + // Model filter — uses the models_used projection (includes zero-cost/BYOK + // models, which the usage_log ledger drops), preserving prior behavior. if (filters.model) { - conditions.push(sql`${workflowExecutionLogs.cost}->>'models' ? ${filters.model}`) + conditions.push(sql`${workflowExecutionLogs.modelsUsed} @> ARRAY[${filters.model}]::text[]`) } // Combine all conditions with AND diff --git a/apps/sim/app/api/v1/logs/route.ts b/apps/sim/app/api/v1/logs/route.ts index 0f8f7b31b82..c85031e3cc4 100644 --- a/apps/sim/app/api/v1/logs/route.ts +++ b/apps/sim/app/api/v1/logs/route.ts @@ -6,7 +6,9 @@ import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v1ListLogsContract } from '@/lib/api/contracts/v1/logs' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildLogFilters, getOrderBy } from '@/app/api/v1/logs/filters' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { @@ -103,6 +105,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, deploymentVersionId: workflowExecutionLogs.deploymentVersionId, level: workflowExecutionLogs.level, @@ -110,7 +113,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, executionData: params.details === 'full' ? workflowExecutionLogs.executionData : sql`null`, workflowName: workflow.name, @@ -144,7 +147,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) } - const formattedLogs = data.map((log) => { + // Only materialize externalized execution data when the response actually + // needs it (details=full + finalOutput/traceSpans requested). + const needsMaterialize = + params.details === 'full' && (params.includeFinalOutput || params.includeTraceSpans) + + const buildBase = (log: (typeof data)[number]) => { const result: any = { id: log.id, workflowId: log.workflowId, @@ -155,7 +163,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: log.startedAt.toISOString(), endedAt: log.endedAt?.toISOString() || null, totalDurationMs: log.totalDurationMs, - cost: log.cost ? { total: (log.cost as any).total } : null, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, files: log.files || null, } @@ -166,24 +174,36 @@ export const GET = withRouteHandler(async (request: NextRequest) => { description: log.workflowDescription, deleted: !log.workflowName, } - - if (log.cost) { - result.cost = log.cost - } - - if (log.executionData) { - const execData = log.executionData as any - if (params.includeFinalOutput && execData.finalOutput) { - result.finalOutput = execData.finalOutput - } - if (params.includeTraceSpans && execData.traceSpans) { - result.traceSpans = execData.traceSpans - } - } } return result - }) + } + + // Only run the bounded-concurrency materialization when the response actually + // needs object-storage reads; otherwise a plain synchronous map avoids the + // per-row worker/promise overhead. + const formattedLogs = needsMaterialize + ? await mapWithConcurrency(data, MATERIALIZE_CONCURRENCY, async (log) => { + const result = buildBase(log) + if (log.executionData) { + const execData = (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as any + if (params.includeFinalOutput && execData.finalOutput) { + result.finalOutput = execData.finalOutput + } + if (params.includeTraceSpans && execData.traceSpans) { + result.traceSpans = execData.traceSpans + } + } + return result + }) + : data.map(buildBase) const limits = await getUserLimits(userId) diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts index 6efff82a7cc..fbbdb0abb5f 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts @@ -9,6 +9,7 @@ import { } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import type { PausePoint } from '@/executor/types' @@ -117,6 +118,7 @@ export const GET = withRouteHandler( .select({ executionId: workflowExecutionLogs.executionId, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, status: workflowExecutionLogs.status, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, @@ -124,7 +126,7 @@ export const GET = withRouteHandler( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, }) .from(workflowExecutionLogs) .where( @@ -177,13 +179,20 @@ export const GET = withRouteHandler( } } - const cost = logRow.cost - ? { total: Number((logRow.cost as { total?: number }).total ?? 0) } - : null + const cost = logRow.costTotal != null ? { total: Number(logRow.costTotal) } : null - const error = status === 'failed' ? extractError(logRow.executionData) : null + // Heavy execution data may live in object storage; resolve the pointer + // before reading error / finalOutput / traceSpans (no-op for inline rows). + const executionData = (await materializeExecutionData( + logRow.executionData as Record | null, + { + workspaceId: logRow.workspaceId, + workflowId: logRow.workflowId, + executionId: logRow.executionId, + } + )) as ExecutionDataShape | undefined - const executionData = logRow.executionData as ExecutionDataShape | undefined + const error = status === 'failed' ? extractError(executionData) : null const finalOutput = includeOutput && status === 'completed' && executionData diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 07a3cf78181..5a170bb9485 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -55,6 +55,13 @@ const MIN_BAR_PCT = 0.5 interface TraceViewProps { traceSpans: TraceSpan[] + /** + * Authoritative, multiplier-inclusive run cost (dollars) from the persisted + * execution log. When provided it drives the header credit chip so the Trace + * tab and the Overview cost breakdown can never show different totals. Falls + * back to the root span's own cost only when absent (e.g. live previews). + */ + runCostDollars?: number } interface FlatSpanEntry { @@ -708,8 +715,10 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa if (cacheRead) metaEntries.push({ label: 'Cache read', value: cacheRead }) if (cacheWrite) metaEntries.push({ label: 'Cache write', value: cacheWrite }) if (reasoning) metaEntries.push({ label: 'Reasoning tokens', value: reasoning }) - const costTotal = formatCostAmount(span.cost?.total) - if (costTotal) metaEntries.push({ label: 'Cost', value: costTotal }) + // Per-span cost is intentionally not shown: cost lives only in the usage_log + // ledger (the authoritative, multiplier-inclusive run total drives the header + // chip). Persisted spans are cost-stripped, so a per-span row would render on + // live runs but vanish on reload — show one consistent total instead. if (span.errorType) metaEntries.push({ label: 'Error type', value: span.errorType }) if (span.iterationIndex !== undefined) metaEntries.push({ label: 'Iteration', value: String(span.iterationIndex + 1) }) @@ -812,7 +821,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa * in a way that mirrors the executor's internal structure so investigators can * follow block-by-block and segment-by-segment what happened and why. */ -export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps) { +export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: TraceViewProps) { const treeRef = useRef(null) const [searchQuery, setSearchQuery] = useState('') const [treePaneWidth, setTreePaneWidth] = useState(DEFAULT_TREE_PANE_WIDTH) @@ -1021,7 +1030,7 @@ export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps) {blockCount} {blockCount === 1 ? 'span' : 'spans'} {(() => { - const rootCost = formatCostAmount(normalizedSpans[0]?.cost?.total) + const rootCost = formatCostAmount(runCostDollars ?? normalizedSpans[0]?.cost?.total) return rootCost ? ( {rootCost} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index a4bf55ea765..615b885a0cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -24,6 +24,7 @@ import { } from '@/components/emcn' import type { WorkflowLogRow } from '@/lib/api/contracts/logs' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' +import { apportionCredits, dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans' @@ -49,6 +50,16 @@ import { formatCost } from '@/providers/utils' import { useLogDetailsUIStore } from '@/stores/logs/store' import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils' +/** + * Renders an already-apportioned integer credit value. `dollars` is only used + * to distinguish a genuine zero ("0 credits") from a sub-credit charge that + * rounded down to zero ("<1 credit"); the credit figure itself is authoritative. + */ +function creditLabel(credits: number, dollars: number): string { + if (credits <= 0) return dollars > 0 ? '<1 credit' : '0 credits' + return `${credits.toLocaleString()} ${credits === 1 ? 'credit' : 'credits'}` +} + export const WorkflowOutputSection = memo( function WorkflowOutputSection({ output }: { output: Record }) { const contentRef = useRef(null) @@ -326,6 +337,54 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP return { input: raw } as Record }, [log.executionData]) + // Cost breakdown, sourced solely from the usage_log ledger (single source of + // truth). Line items (Base Run / per-model / per-integration) get integer + // credits apportioned with a single round at the total so rows always + // reconcile (never round-then-sum, which drifts). Pre-ledger runs that only + // have the cost_total projection show the total alone — no itemization, no + // parallel jsonb reconstruction. + const costBreakdown = useMemo((): { + rows: Array<{ key: string; label: string; credits: number; dollars: number }> + totalCredits: number + totalDollars: number + tokens: { input: number; output: number } + } | null => { + const ledger = log.costLedger + if (ledger && ledger.items.length > 0) { + const credits = apportionCredits( + ledger.items.map((item, i) => ({ key: String(i), dollars: item.cost })) + ) + const rows = ledger.items.map((item, i) => ({ + key: String(i), + label: + item.category === 'fixed' && item.description === 'execution_fee' + ? 'Base Run' + : item.description, + credits: credits[String(i)] ?? 0, + dollars: item.cost, + })) + return { + rows, + totalCredits: dollarsToCredits(ledger.total), + totalDollars: ledger.total, + tokens: { + input: ledger.items.reduce((s, it) => s + (it.inputTokens ?? 0), 0), + output: ledger.items.reduce((s, it) => s + (it.outputTokens ?? 0), 0), + }, + } + } + + // Total-only (pre-ledger runs with just the cost_total projection). + const total = log.cost?.total + if (total == null) return null + return { + rows: [], + totalCredits: dollarsToCredits(total), + totalDollars: total, + tokens: { input: 0, output: 0 }, + } + }, [log.costLedger, log.cost]) + const formattedTimestamp = formatDate(log.createdAt) const logStatus = getDisplayStatus(log.status) @@ -529,67 +588,39 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {log.files && log.files.length > 0 && } {/* Cost Breakdown */} - {hasCostInfo && ( + {hasCostInfo && costBreakdown && (
-
- - Base Run - - - {formatCost(BASE_EXECUTION_CHARGE)} - -
-
- - Model Input - - - {formatCost(log.cost?.input || 0)} - -
-
- - Model Output - - - {formatCost(log.cost?.output || 0)} - -
- {(() => { - const models = (log.cost as Record)?.models as - | Record - | undefined - const totalToolCost = models - ? Object.values(models).reduce((sum, m) => sum + (m?.toolCost || 0), 0) - : 0 - return totalToolCost > 0 ? ( -
- - Tool Usage - - - {formatCost(totalToolCost)} - -
- ) : null - })()} + {costBreakdown.rows.map((row) => ( +
+ + {row.label} + + + {creditLabel(row.credits, row.dollars)} + +
+ ))}
Total - {formatCost(log.cost?.total || 0)} - -
-
- - Tokens - - - {log.cost?.tokens?.input || log.cost?.tokens?.prompt || 0} in ·{' '} - {log.cost?.tokens?.output || log.cost?.tokens?.completion || 0} out + {creditLabel(costBreakdown.totalCredits, costBreakdown.totalDollars)}
+ {(costBreakdown.tokens.input > 0 || costBreakdown.tokens.output > 0) && ( +
+ + Tokens + + + {costBreakdown.tokens.input} in · {costBreakdown.tokens.output} out + +
+ )}

Total includes a {formatCost(BASE_EXECUTION_CHARGE)} base charge plus model and @@ -608,7 +639,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP className='mt-3 min-h-0 flex-1 overflow-hidden focus-visible:outline-none' > {traceSpans?.length ? ( - + ) : log.executionData ? (

diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 4f142b0c67d..09ead88ac8d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -1,6 +1,6 @@ import type React from 'react' import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { formatCreditCost } from '@/lib/billing/credits/conversion' import type { TraceSpan } from '@/lib/logs/types' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' @@ -117,10 +117,7 @@ export function getDisplayName(span: TraceSpan): string { } export function formatCostAmount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - const credits = dollarsToCredits(value) - if (credits <= 0) return '<1 credit' - return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` + return formatCreditCost(value, { emptyForZeroOrLess: true }) } export function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index 4aaf902ffaf..998f1c5dcf9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -19,6 +19,7 @@ import { AnthropicIcon, BrandfetchIcon, ExaAIIcon, + FindymailIcon, FirecrawlIcon, FireworksIcon, GeminiIcon, @@ -32,7 +33,9 @@ import { ParallelIcon, PeopleDataLabsIcon, PerplexityIcon, + ProspeoIcon, SerperIcon, + WizaIcon, } from '@/components/icons' import { Input } from '@/components/ui' import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' @@ -172,6 +175,27 @@ const PROVIDERS: { description: 'Person and company enrichment, search, and identity', placeholder: 'Enter your People Data Labs API key', }, + { + id: 'findymail', + name: 'Findymail', + icon: FindymailIcon, + description: 'Email finder, verification, and phone lookup', + placeholder: 'Enter your Findymail API key', + }, + { + id: 'prospeo', + name: 'Prospeo', + icon: ProspeoIcon, + description: 'Person and company enrichment and search', + placeholder: 'Enter your Prospeo API key', + }, + { + id: 'wiza', + name: 'Wiza', + icon: WizaIcon, + description: 'Prospect search, individual reveal, and company enrichment', + placeholder: 'Enter your Wiza API key', + }, ] export function BYOK() { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx index 4905edb3035..d7651bd3c64 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx @@ -53,8 +53,8 @@ export function ColumnConfigSidebar(props: ColumnConfigSidebarProps) { role='dialog' aria-label='Configure column' className={cn( - 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out', - open ? 'translate-x-0' : 'translate-x-full' + 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] transition-transform duration-200 ease-out', + open ? 'translate-x-0 shadow-overlay' : 'translate-x-full' )} > {props.config && ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx index 2e021797858..257c66861af 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx @@ -3,7 +3,6 @@ import { useState } from 'react' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { X } from 'lucide-react' import { Badge, Button, @@ -15,7 +14,7 @@ import { Switch, toast, } from '@/components/emcn' -import { ArrowLeft } from '@/components/emcn/icons' +import { ArrowLeft, X } from '@/components/emcn/icons' import type { AddWorkflowGroupBodyInput } from '@/lib/api/contracts/tables' import { cn } from '@/lib/core/utils/cn' import type { ColumnDefinition, WorkflowGroup, WorkflowGroupOutput } from '@/lib/table' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx index 3594d0ca029..a2575b4b43f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx @@ -1,9 +1,8 @@ 'use client' import { useState } from 'react' -import { X } from 'lucide-react' -import { Button, Input } from '@/components/emcn' -import { Search } from '@/components/emcn/icons' +import { Input } from '@/components/emcn' +import { Search, X } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' import { ALL_ENRICHMENTS } from '@/enrichments' @@ -33,8 +32,8 @@ export function EnrichmentsSidebar({ open, ...rest }: EnrichmentsSidebarProps) { role='dialog' aria-label='Enrichments' className={cn( - 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out', - open ? 'translate-x-0' : 'translate-x-full' + 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] transition-transform duration-200 ease-out', + open ? 'translate-x-0 shadow-overlay' : 'translate-x-full' )} > {open && } @@ -75,15 +74,14 @@ function EnrichmentsSidebarBody({

Enrichment

- +

@@ -121,15 +119,14 @@ function EnrichmentsSidebarBody({

Enrichments

- +
@@ -155,11 +152,10 @@ function EnrichmentsSidebarBody({ const Icon = enrichment.icon return (
  • - +
  • ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx index 60c3cc05336..54a2c7f2dea 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx @@ -10,6 +10,9 @@ interface CellContentProps { value: unknown exec?: RowExecutionMetadata column: DisplayColumn + /** Current workspace id — lets string cells holding an in-workspace resource + * URL render as a tagged-resource chip instead of a plain external link. */ + workspaceId: string isEditing: boolean initialCharacter?: string | null onSave: (value: unknown, reason: SaveReason) => void @@ -34,6 +37,7 @@ export function CellContent({ value, exec, column, + workspaceId, isEditing, initialCharacter, onSave, @@ -41,7 +45,14 @@ export function CellContent({ waitingOnLabels, isEnrichmentOutput, }: CellContentProps) { - const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput }) + const kind = resolveCellRender({ + value, + exec, + column, + waitingOnLabels, + isEnrichmentOutput, + currentWorkspaceId: workspaceId, + }) return ( <> diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 557186b7668..fe6a6bfd3da 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table' import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils' import { storageToDisplay } from '../../../utils' import type { DisplayColumn } from '../types' +import { SimResourceCell, type SimResourceType } from './sim-resource-cell' export type CellRenderKind = // Workflow-output cells @@ -26,6 +27,13 @@ export type CellRenderKind = | { kind: 'json'; text: string } | { kind: 'date'; text: string } | { kind: 'url'; text: string; href: string; domain: string } + | { + kind: 'sim-resource' + workspaceId: string + resourceType: SimResourceType + resourceId: string + href: string + } | { kind: 'text'; text: string } // Universal fallback | { kind: 'empty' } @@ -38,6 +46,9 @@ interface ResolveCellRenderInput { /** Column is an enrichment-group output — a completed-but-empty cell renders * "Not found" rather than a blank, since the enrichment ran and matched nothing. */ isEnrichmentOutput?: boolean + /** Current workspace id — a URL pointing to a resource in this workspace + * renders as a tagged-resource chip rather than a plain external link. */ + currentWorkspaceId?: string } export function resolveCellRender({ @@ -46,6 +57,7 @@ export function resolveCellRender({ column, waitingOnLabels, isEnrichmentOutput, + currentWorkspaceId, }: ResolveCellRenderInput): CellRenderKind { const isNull = value === null || value === undefined const isEmpty = isNull || value === '' @@ -97,6 +109,18 @@ export function resolveCellRender({ if (column.type === 'date') return { kind: 'date', text: String(value) } if (column.type === 'string') { const text = stringifyValue(value) + if (currentWorkspaceId) { + const resource = extractSimResourceInfo(text) + if (resource && resource.workspaceId === currentWorkspaceId) { + return { + kind: 'sim-resource', + workspaceId: resource.workspaceId, + resourceType: resource.resourceType, + resourceId: resource.resourceId, + href: resource.href, + } + } + } const urlInfo = extractUrlInfo(text) if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } return { kind: 'text', text } @@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null { return null } +/** Maps a workspace route section to the sim resource kind it addresses. */ +const SIM_RESOURCE_SECTIONS: Record = { + w: 'workflow', + tables: 'table', + knowledge: 'knowledge', + files: 'file', +} + +/** + * Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or + * relative) pointing to a sim resource and returns its descriptor. The href is + * the pathname so the link stays within the current deployment. Returns null + * for anything that isn't a single-segment resource route. + */ +function extractSimResourceInfo( + text: string +): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null { + const trimmed = text.trim() + if (!trimmed) return null + let pathname: string + if (/^https?:\/\//i.test(trimmed)) { + try { + pathname = new URL(trimmed).pathname + } catch { + return null + } + } else if (trimmed.startsWith('/')) { + pathname = trimmed.split(/[?#]/)[0] + } else { + return null + } + const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/) + if (!match) return null + const [, workspaceId, section, resourceId] = match + return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname } +} + interface CellRenderProps { kind: CellRenderKind isEditing: boolean @@ -259,7 +320,7 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle target='_blank' rel='noopener noreferrer' className={cn( - 'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70', + 'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 transition-colors hover-hover:text-[var(--text-secondary)]', isEditing && 'pointer-events-none' )} onClick={(e) => e.stopPropagation()} @@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle ) + case 'sim-resource': + return ( + + ) + case 'text': return ( = { + workflow: 'Workflow', + table: 'Table', + knowledge: 'Knowledge base', + file: 'File', +} + +interface SimResourceCellProps { + /** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */ + workspaceId: string + resourceType: SimResourceType + resourceId: string + /** In-app pathname the resource link navigates to. */ + href: string + isEditing: boolean +} + +/** + * Renders a cell whose value is a URL pointing to a sim resource in the current + * workspace as a tagged-resource chip — the same icon (and per-workflow colored + * square) used for @-style resource mentions, plus the resource's name as a link. + * Only the list matching `resourceType` is fetched; the other queries stay + * disabled so a sim-resource cell subscribes to a single shared list. + */ +export function SimResourceCell({ + workspaceId, + resourceType, + resourceId, + href, + isEditing, +}: SimResourceCellProps) { + const { data: workflows = [] } = useWorkflows( + resourceType === 'workflow' ? workspaceId : undefined + ) + const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined) + const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, { + enabled: resourceType === 'knowledge', + }) + const { data: files = [] } = useWorkspaceFiles(workspaceId, 'active', { + enabled: resourceType === 'file', + }) + + const workflow = + resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined + + const name = useMemo(() => { + switch (resourceType) { + case 'workflow': + return workflow?.name + case 'table': + return tables.find((t) => t.id === resourceId)?.name + case 'knowledge': + return knowledgeBases.find((kb) => kb.id === resourceId)?.name + case 'file': + return files.find((f) => f.id === resourceId)?.name + } + }, [resourceType, resourceId, workflow, tables, knowledgeBases, files]) + + const label = name ?? FALLBACK_LABEL[resourceType] + + const context: ChatMessageContext = + resourceType === 'workflow' + ? { kind: 'workflow', label, workflowId: resourceId } + : resourceType === 'table' + ? { kind: 'table', label, tableId: resourceId } + : resourceType === 'knowledge' + ? { kind: 'knowledge', label, knowledgeId: resourceId } + : { kind: 'file', label, fileId: resourceId } + + return ( + + + e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + > + {label} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index e228edba84d..219a3376e78 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils' export interface DataRowProps { row: TableRowType columns: DisplayColumn[] + /** Current workspace id — forwarded to cells so in-workspace resource URLs + * render as tagged-resource chips. */ + workspaceId: string rowIndex: number isFirstRow: boolean editingColumnName: string | null @@ -57,6 +60,10 @@ export interface DataRowProps { * queued indicators across page refresh during long Run-all dispatches. */ activeDispatches: ActiveDispatch[] | undefined + /** Pixel `left` value for each pinned column key; absent keys are not pinned. */ + pinnedOffsets?: Map + /** Key of the rightmost pinned column, used to render a separator shadow. */ + lastPinnedColKey?: string | null } function cellRangeRowChanged( @@ -94,6 +101,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { if ( prev.row !== next.row || prev.columns !== next.columns || + prev.workspaceId !== next.workspaceId || prev.rowIndex !== next.rowIndex || prev.isFirstRow !== next.isFirstRow || prev.editingColumnName !== next.editingColumnName || @@ -113,7 +121,9 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { prev.onStopRow !== next.onStopRow || prev.onRunRow !== next.onRunRow || prev.workflowGroups !== next.workflowGroups || - prev.activeDispatches !== next.activeDispatches + prev.activeDispatches !== next.activeDispatches || + prev.pinnedOffsets !== next.pinnedOffsets || + prev.lastPinnedColKey !== next.lastPinnedColKey ) { return false } @@ -135,6 +145,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { export const DataRow = React.memo(function DataRow({ row, columns, + workspaceId, rowIndex, isFirstRow, editingColumnName, @@ -157,6 +168,8 @@ export const DataRow = React.memo(function DataRow({ onRunRow, workflowGroups, activeDispatches, + pinnedOffsets, + lastPinnedColKey, }: DataRowProps) { const sel = normalizedSelection /** @@ -196,7 +209,12 @@ export const DataRow = React.memo(function DataRow({ tabIndex={0} aria-checked={isRowSelected} aria-label={`Select row ${rowIndex + 1}`} - className='group/checkbox flex h-[20px] shrink-0 items-center justify-center' + className={cn( + 'group/checkbox flex h-[20px] shrink-0 items-center justify-end', + // Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28); + // full 4px once the column widens (4+ digits, numDivWidth ≥ 36). + numDivWidth >= 36 ? 'pr-1' : 'pr-0.5' + )} style={{ width: numDivWidth }} onMouseDown={(e) => { if (e.button !== 0) return @@ -208,7 +226,7 @@ export const DataRow = React.memo(function DataRow({ > @@ -264,13 +282,23 @@ export const DataRow = React.memo(function DataRow({ const isLeftEdge = inRange ? colIndex === sel!.startCol : colIndex === 0 const isRightEdge = inRange ? colIndex === sel!.endCol : colIndex === columns.length - 1 + const pinnedLeft = pinnedOffsets?.get(column.key) + const isPinnedCell = pinnedLeft !== undefined + const isPinnedSeparator = column.key === lastPinnedColKey + return ( { if (e.button !== 0 || isEditing) return onCellMouseDown(rowIndex, colIndex, e.shiftKey) @@ -310,6 +338,7 @@ export const DataRow = React.memo(function DataRow({ )}
    void onRenameCancel: () => void onColumnSelect: (colIndex: number, shiftKey: boolean) => void - onChangeType: (columnName: string, newType: ColumnDefinition['type']) => void onInsertLeft: (columnName: string) => void onInsertRight: (columnName: string) => void onDeleteColumn: (columnName: string) => void @@ -42,6 +41,14 @@ interface ColumnHeaderMenuProps { /** Opens a popup preview of the column's underlying workflow. Surfaced in * the chevron menu for workflow-output columns. */ onViewWorkflow?: (workflowId: string) => void + /** Whether this column is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state for this column. */ + onPinToggle?: (columnName: string) => void + /** Left offset in pixels when pinned (drives `position: sticky`). */ + stickyLeft?: number + /** Whether this is the rightmost pinned column (renders a separator shadow). */ + isLastPinned?: boolean } /** @@ -76,6 +83,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ sourceInfo, onOpenConfig, onViewWorkflow, + isPinned, + onPinToggle, + stickyLeft, + isLastPinned, }: ColumnHeaderMenuProps) { const renameInputRef = useRef(null) const didDragRef = useRef(false) @@ -228,7 +239,12 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ return ( - + onViewWorkflow(ownGroup.workflowId) : undefined } + isPinned={isPinned} + onPinToggle={onPinToggle} />
    )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 211c3e0a55a..56468fb1f61 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -1,7 +1,7 @@ 'use client' import type React from 'react' -import { useCallback, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { DropdownMenu, DropdownMenuContent, @@ -18,6 +18,8 @@ import { Eye, EyeOff, Pencil, + Pin, + PinOff, PlayOutline, Trash, } from '@/components/emcn/icons' @@ -67,6 +69,10 @@ interface ColumnOptionsMenuProps { /** When set, the menu surfaces a "View workflow" item that opens a popup * preview of the configured workflow. */ onViewWorkflow?: () => void + /** Whether this column is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state of this column. */ + onPinToggle?: (columnName: string) => void } /** @@ -93,6 +99,8 @@ export function ColumnOptionsMenu({ onRunColumnSelected, selectedRowCount = 0, onViewWorkflow, + isPinned, + onPinToggle, }: ColumnOptionsMenuProps) { const showRunActions = Boolean(onRunColumnAll && onRunColumnIncomplete) const showRunSelected = Boolean(onRunColumnSelected) && selectedRowCount > 0 @@ -159,6 +167,12 @@ export function ColumnOptionsMenu({ Edit column + {onPinToggle && ( + onPinToggle(column.name)}> + {isPinned ? : } + {isPinned ? 'Unpin column' : 'Pin column'} + + )} onInsertLeft(column.name)}> @@ -219,6 +233,14 @@ interface WorkflowGroupMetaCellProps { onDragEnd?: () => void onDragLeave?: () => void readOnly?: boolean + /** Left offset in pixels when pinned (drives `position: sticky`). */ + stickyLeft?: number + /** Whether this is the rightmost pinned column group (renders a separator shadow). */ + isLastPinned?: boolean + /** Whether this column group is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state for this column group. */ + onPinToggle?: (columnName: string) => void } /** @@ -252,6 +274,10 @@ export function WorkflowGroupMetaCell({ onDragEnd, onDragLeave, readOnly, + stickyLeft, + isLastPinned, + isPinned, + onPinToggle, }: WorkflowGroupMetaCellProps) { const isEnrichment = groupType === 'enrichment' const enrichment = isEnrichment ? getEnrichment(enrichmentId) : undefined @@ -269,112 +295,94 @@ export function WorkflowGroupMetaCell({ const selectedCount = selectedRowIds?.length ?? 0 - const handleRunAll = useCallback(() => { + function handleRunAll() { if (groupId) onRunColumn?.(groupId, 'all') - }, [groupId, onRunColumn]) + } - const handleRunIncomplete = useCallback(() => { + function handleRunIncomplete() { if (groupId) onRunColumn?.(groupId, 'incomplete') - }, [groupId, onRunColumn]) + } - const handleRunSelected = useCallback(() => { + function handleRunSelected() { if (groupId && selectedRowIds && selectedRowIds.length > 0) { onRunColumn?.(groupId, 'all', selectedRowIds) } - }, [groupId, onRunColumn, selectedRowIds]) + } - const handleRunLimited = useCallback( - (max: number) => { - if (groupId) onRunColumn?.(groupId, 'incomplete', undefined, { type: 'rows', max }) - }, - [groupId, onRunColumn] - ) + function handleRunLimited(max: number) { + if (groupId) onRunColumn?.(groupId, 'incomplete', undefined, { type: 'rows', max }) + } - const handleContextMenu = useCallback( - (e: React.MouseEvent) => { - if (!column) return - e.preventDefault() - e.stopPropagation() - setOptionsMenuPosition({ x: e.clientX, y: e.clientY }) - setOptionsMenuOpen(true) - }, - [column] - ) + function handleContextMenu(e: React.MouseEvent) { + if (!column) return + e.preventDefault() + e.stopPropagation() + setOptionsMenuPosition({ x: e.clientX, y: e.clientY }) + setOptionsMenuOpen(true) + } - const selectGroupAndOpenConfig = useCallback( - (e: React.MouseEvent) => { - // Ignore clicks that landed on an interactive child (badge, play button, - // dropdown items rendered via portal). Only the bare meta-cell area - // should select the group + open the config sidebar. - const target = e.target as HTMLElement - if (target.closest('button, [role="menuitem"], [role="menu"]')) return - // Drag-vs-click guard: when a drag just ended on this cell, swallow the - // synthetic click so we don't accidentally pop open the sidebar. - if (didDragRef.current) { - didDragRef.current = false - return - } - onSelectGroup(startColIndex, size) - if (columnName) onOpenConfig(columnName) - }, - [columnName, onOpenConfig, onSelectGroup, size, startColIndex] - ) + function selectGroupAndOpenConfig(e: React.MouseEvent) { + // Ignore clicks that landed on an interactive child (badge, play button, + // dropdown items rendered via portal). Only the bare meta-cell area + // should select the group + open the config sidebar. + const target = e.target as HTMLElement + if (target.closest('button, [role="menuitem"], [role="menu"]')) return + // Drag-vs-click guard: when a drag just ended on this cell, swallow the + // synthetic click so we don't accidentally pop open the sidebar. + if (didDragRef.current) { + didDragRef.current = false + return + } + onSelectGroup(startColIndex, size) + if (columnName) onOpenConfig(columnName) + } - const handleDragStart = useCallback( - (e: React.DragEvent) => { - if (readOnly || !onDragStart || !columnName) { - e.preventDefault() - return - } - didDragRef.current = true - e.dataTransfer.effectAllowed = 'move' - e.dataTransfer.setData('text/plain', columnName) + function handleDragStart(e: React.DragEvent) { + if (readOnly || !onDragStart || !columnName) { + e.preventDefault() + return + } + didDragRef.current = true + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', columnName) - const ghost = document.createElement('div') - ghost.textContent = name - ghost.style.cssText = - 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)' - document.body.appendChild(ghost) - e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) - requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) + const ghost = document.createElement('div') + ghost.textContent = name + ghost.style.cssText = + 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)' + document.body.appendChild(ghost) + e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) + requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) - onDragStart(columnName) - }, - [columnName, name, onDragStart, readOnly] - ) + onDragStart(columnName) + } - const handleDragOver = useCallback( - (e: React.DragEvent) => { - if (!onDragOver || !columnName) return - e.preventDefault() - e.dataTransfer.dropEffect = 'move' - const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() - const midX = rect.left + rect.width / 2 - const side = e.clientX < midX ? 'left' : 'right' - onDragOver(columnName, side) - }, - [columnName, onDragOver] - ) + function handleDragOver(e: React.DragEvent) { + if (!onDragOver || !columnName) return + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + const midX = rect.left + rect.width / 2 + const side = e.clientX < midX ? 'left' : 'right' + onDragOver(columnName, side) + } - const handleDragEnd = useCallback(() => { + function handleDragEnd() { didDragRef.current = false onDragEnd?.() - }, [onDragEnd]) + } - const handleDragLeave = useCallback( - (e: React.DragEvent) => { - const th = e.currentTarget as HTMLElement - const related = e.relatedTarget as Node | null - if (related && th.contains(related)) return - if (related && related instanceof Element && related.closest('th')) return - onDragLeave?.() - }, - [onDragLeave] - ) + function handleDragLeave(e: React.DragEvent) { + const th = e.currentTarget as HTMLElement + const related = e.relatedTarget as Node | null + if (related && th.contains(related)) return + if (related && related instanceof Element && related.closest('th')) return + onDragLeave?.() + } - const handleDrop = useCallback((e: React.DragEvent) => { + function handleDrop(e: React.DragEvent) { e.preventDefault() - }, []) + } const isDraggable = !readOnly && Boolean(onDragStart) @@ -389,7 +397,12 @@ export function WorkflowGroupMetaCell({ onDragEnd={isDraggable ? handleDragEnd : undefined} onDragLeave={isDraggable ? handleDragLeave : undefined} onDrop={isDraggable ? handleDrop : undefined} - className='group relative cursor-pointer border-[var(--border)] border-r border-b bg-[var(--bg)] px-2 py-[5px] text-left align-middle before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[-1px] before:w-px before:bg-[var(--border)] before:content-[""]' + className={cn( + 'group relative cursor-pointer border-[var(--border)] border-r border-b bg-[var(--bg)] px-2 py-[5px] text-left align-middle before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[-1px] before:w-px before:bg-[var(--border)] before:content-[""]', + stickyLeft !== undefined && 'z-[11]', + isLastPinned && '[box-shadow:2px_0_0_0_var(--border)]' + )} + style={stickyLeft !== undefined ? { position: 'sticky', left: stickyLeft } : undefined} >
    0 ? handleRunSelected : undefined} selectedRowCount={selectedCount} onViewWorkflow={onViewWorkflow ? () => onViewWorkflow(workflowId) : undefined} + isPinned={isPinned} + onPinToggle={onPinToggle} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index d75b63c9ebb..e38c2fbdcbe 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -7,7 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { Skeleton, toast, useToast } from '@/components/emcn' -import { TableX } from '@/components/emcn/icons' +import { Loader, TableX } from '@/components/emcn/icons' import type { RunLimit, RunMode } from '@/lib/api/contracts/tables' import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' @@ -40,7 +40,6 @@ import { import type { ColumnConfig } from '../column-config-sidebar' import { ContextMenu } from '../context-menu' import { NewColumnDropdown } from '../new-column-dropdown' -import { RunStatusControl } from '../run-status-control' import type { WorkflowConfig } from '../workflow-sidebar' import { ExpandedCellPopover } from './cells' import { ADD_COL_WIDTH, CELL_HEADER_CHECKBOX, COL_WIDTH, SELECTION_TINT_BG } from './constants' @@ -160,10 +159,6 @@ interface TableGridProps { onStopRows: (rowIds: string[]) => void /** Single-row stop for the per-row gutter button. */ onStopRow: (rowId: string) => void - /** Wholesale cancel — page-header "Stop all". */ - onStopAll: () => void - /** Whether `useCancelTableRuns` is currently in flight. */ - cancelRunsPending: boolean /** * Fired whenever the action-bar selection or running-count derivations * change. Wrapper uses this to render . @@ -258,8 +253,6 @@ export function TableGrid({ onRunRows, onStopRows, onStopRow, - onStopAll, - cancelRunsPending, onSelectionChange, queryOptions, columnRenameSinkRef, @@ -302,6 +295,9 @@ export function TableGrid({ const [dropSide, setDropSide] = useState<'left' | 'right'>('left') const dropSideRef = useRef(dropSide) dropSideRef.current = dropSide + const [pinnedColumns, setPinnedColumns] = useState([]) + const pinnedColumnsRef = useRef(pinnedColumns) + pinnedColumnsRef.current = pinnedColumns const metadataSeededRef = useRef(false) const containerRef = useRef(null) const scrollRef = useRef(null) @@ -330,8 +326,13 @@ export function TableGrid({ const { data: tableRunState } = useTableRunState(tableId) const activeDispatches = tableRunState?.dispatches - const totalRunning = tableRunState?.runningCellCount ?? 0 const runningByRowId = tableRunState?.runningByRowId ?? EMPTY_RUNNING_BY_ROW + // Actual in-flight cell count = sum of the live per-row map (kept current by + // applyCell's SSE deltas, and the same source the per-row gutter uses). The + // dispatch-scope `runningCellCount` over-counts already-completed groups on + // rows still inside a dispatch's scope — e.g. a cascade where 3 of 4 columns + // finished would read "4 running" instead of "1". + const totalRunning = Object.values(runningByRowId).reduce((sum, n) => sum + n, 0) const tableRowCountRef = useRef(tableData?.rowCount ?? 0) tableRowCountRef.current = tableData?.rowCount ?? 0 @@ -466,9 +467,13 @@ export function TableGrid({ } const updatedOrder = columnOrderRef.current?.map((n) => (n === oldName ? newName : n)) if (updatedOrder) setColumnOrder(updatedOrder) + const updatedPinned = pinnedColumnsRef.current.map((n) => (n === oldName ? newName : n)) + const pinnedChanged = updatedPinned.some((n, i) => n !== pinnedColumnsRef.current[i]) + if (pinnedChanged) setPinnedColumns(updatedPinned) updateMetadataRef.current({ columnWidths: updatedWidths, ...(updatedOrder ? { columnOrder: updatedOrder } : {}), + ...(pinnedChanged ? { pinnedColumns: updatedPinned } : {}), }) } // Populate the wrapper's sink so its sidebars can fire renames back into @@ -483,12 +488,61 @@ export function TableGrid({ setColumnWidths(widths) } + function handlePinnedColumnsChange(pinned: string[]) { + setPinnedColumns(pinned) + pinnedColumnsRef.current = pinned + } + + function getPinnedColumns() { + return pinnedColumnsRef.current + } + + const handlePinToggle = useCallback((columnName: string) => { + const col = columnsRef.current.find((c) => c.name === columnName) + const siblings: string[] = col?.workflowGroupId + ? columnsRef.current + .filter((c) => c.workflowGroupId === col.workflowGroupId) + .map((c) => c.name) + : [columnName] + + const current = pinnedColumnsRef.current + const newPinned = current.includes(columnName) + ? current.filter((n) => !siblings.includes(n)) + : [...current, ...siblings.filter((n) => !current.includes(n))] + setPinnedColumns(newPinned) + pinnedColumnsRef.current = newPinned + + // Pinned-at-front is an invariant the rest of the grid relies on (sticky + // offsets walk displayColumns left→right and stop at the first unpinned + // entry). On unpin we must re-sort so the unpinned column doesn't stay + // sandwiched between still-pinned siblings, which would render the sticky + // zone with a gap. + const currentOrder = columnOrderRef.current ?? schemaColumnsRef.current.map((c) => c.name) + const pinnedSet = new Set(newPinned) + const newOrder = [ + ...currentOrder.filter((n) => pinnedSet.has(n)), + ...currentOrder.filter((n) => !pinnedSet.has(n)), + ] + const orderChanged = newOrder.some((n, i) => n !== currentOrder[i]) + if (orderChanged) { + setColumnOrder(newOrder) + columnOrderRef.current = newOrder + } + updateMetadataRef.current({ + pinnedColumns: newPinned, + ...(orderChanged ? { columnOrder: newOrder } : {}), + columnWidths: columnWidthsRef.current, + }) + }, []) + const { pushUndo, undo, redo } = useTableUndo({ workspaceId, tableId, onColumnOrderChange: handleColumnOrderChange, onColumnRename: handleColumnRename, onColumnWidthsChange: handleColumnWidthsChange, + onPinnedColumnsChange: handlePinnedColumnsChange, + getPinnedColumns, getColumnWidths, }) const undoRef = useRef(undo) @@ -530,6 +584,49 @@ export function TableGrid({ hasWorkflowColumns ) + const pinnedColumnSet = useMemo(() => new Set(pinnedColumns), [pinnedColumns]) + + // Stable fingerprint of pinned-column widths only. Changes when a pinned + // column is resized; stays the same when an unpinned column is resized. + // Used as the sole dep that ties pinnedOffsets to column-width changes so + // that unpinned resizes don't recreate the Map and re-render all DataRows. + const pinnedWidthsKey = displayColumns + .filter((c) => pinnedColumnSet.has(c.name)) + .map((c) => columnWidths[c.key] ?? COL_WIDTH) + .join(',') + + /** Pinned column key → sticky `left` px offset. */ + const pinnedOffsets = useMemo>(() => { + const offsets = new Map() + let left = checkboxColWidth + const widths = columnWidthsRef.current + for (const col of displayColumns) { + if (pinnedColumnSet.has(col.name)) { + offsets.set(col.key, left) + left += widths[col.key] ?? COL_WIDTH + } + } + return offsets + }, [displayColumns, pinnedColumnSet, checkboxColWidth, pinnedWidthsKey]) + + const lastPinnedColKey = useMemo(() => { + let last: string | null = null + for (const col of displayColumns) { + if (pinnedColumnSet.has(col.name)) last = col.key + } + return last + }, [displayColumns, pinnedColumnSet]) + + /** Right edge of the pinned sticky zone; used as the left inset for scroll-to-reveal. */ + const pinnedStickyLeftEdge = useMemo(() => { + let edge = checkboxColWidth + const widths = columnWidthsRef.current + for (const [key, left] of pinnedOffsets) { + edge = Math.max(edge, left + (widths[key] ?? COL_WIDTH)) + } + return edge + }, [pinnedOffsets, checkboxColWidth]) + const headerGroups = useMemo( () => buildHeaderGroups(displayColumns, tableWorkflowGroups), [displayColumns, tableWorkflowGroups] @@ -1127,6 +1224,16 @@ export function TableGrid({ } } + // Reorder is restricted to within a single zone so a cross-zone drop + // indicator never appears for an insertion the grid would refuse. + if (dragged) { + const pinned = pinnedColumnsRef.current + if (pinned.includes(dragged) !== pinned.includes(columnName)) { + if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) + return + } + } + // Workflow groups: skip per-`` writes and let `handleScrollDragOver` // do the bookkeeping. The scroll handler computes side from the group's // full bounds, so it stays stable across sibling cursor moves; the per-th @@ -1248,17 +1355,29 @@ export function TableGrid({ ...remaining.slice(insertIndex), ] - const orderChanged = newOrder.some((name, i) => currentOrder[i] !== name) + // Belt-and-suspenders re-sort: dragover already blocks cross-zone drops, + // but if anything ever slips through, the pinned-at-front invariant gets + // restored here (relative order within each zone is preserved). + let finalOrder = newOrder + const currentPinned = pinnedColumnsRef.current + if (currentPinned.length > 0) { + const pinnedSet = new Set(currentPinned) + const pinnedInNew = newOrder.filter((n) => pinnedSet.has(n)) + const unpinnedInNew = newOrder.filter((n) => !pinnedSet.has(n)) + finalOrder = [...pinnedInNew, ...unpinnedInNew] + } + + const orderChanged = finalOrder.some((name, i) => currentOrder[i] !== name) if (orderChanged) { pushUndoRef.current({ type: 'reorder-columns', previousOrder: currentOrder, - newOrder, + newOrder: finalOrder, }) - setColumnOrder(newOrder) + setColumnOrder(finalOrder) updateMetadataRef.current({ columnWidths: columnWidthsRef.current, - columnOrder: newOrder, + columnOrder: finalOrder, }) } } @@ -1302,6 +1421,12 @@ export function TableGrid({ if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) return } + const pinned = pinnedColumnsRef.current + const draggedName = dragColumnNameRef.current + if (draggedName && pinned.includes(draggedName) !== pinned.includes(col.name)) { + if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) + return + } const midX = left + groupWidth / 2 const side = cursorX < midX ? 'left' : 'right' if (col.name !== dropTargetColumnNameRef.current || side !== dropSideRef.current) { @@ -1345,8 +1470,13 @@ export function TableGrid({ useEffect(() => { if (!tableData?.metadata) return - if (!tableData.metadata.columnWidths && !tableData.metadata.columnOrder) return - // First load: seed both from the server and remember we've seeded. + if ( + !tableData.metadata.columnWidths && + !tableData.metadata.columnOrder && + !tableData.metadata.pinnedColumns + ) + return + // First load: seed all from the server and remember we've seeded. if (!metadataSeededRef.current) { metadataSeededRef.current = true if (tableData.metadata.columnWidths) { @@ -1355,6 +1485,9 @@ export function TableGrid({ if (tableData.metadata.columnOrder) { setColumnOrder(tableData.metadata.columnOrder) } + if (tableData.metadata.pinnedColumns) { + setPinnedColumns(tableData.metadata.pinnedColumns) + } return } // After first load: only re-seed `columnOrder` when the *set of columns* @@ -1528,7 +1661,7 @@ export function TableGrid({ const selector = `[data-table-scroll] [data-row="${rowIndex}"][data-col="${colIndex}"]` // `scrollIntoView` ignores the sticky `` and sticky gutter, so a cell // scrolled to the edge lands behind them. Scroll manually with insets equal - // to the sticky header height (top) and the row-number column width (left). + // to the sticky header height (top) and the full pinned left edge (left). const revealCell = (cell: HTMLElement) => { const scrollEl = scrollRef.current if (!scrollEl) return @@ -1540,10 +1673,14 @@ export function TableGrid({ } else if (rect.bottom > view.bottom) { scrollEl.scrollTop += rect.bottom - view.bottom } - if (rect.left < view.left + checkboxColWidth) { - scrollEl.scrollLeft -= view.left + checkboxColWidth - rect.left - } else if (rect.right > view.right) { - scrollEl.scrollLeft += rect.right - view.right + const targetColName = columnsRef.current[colIndex]?.name + const targetIsPinned = targetColName ? pinnedColumnSet.has(targetColName) : false + if (!targetIsPinned) { + if (rect.left < view.left + pinnedStickyLeftEdge) { + scrollEl.scrollLeft -= view.left + pinnedStickyLeftEdge - rect.left + } else if (rect.right > view.right) { + scrollEl.scrollLeft += rect.right - view.right + } } } let secondRaf = 0 @@ -1565,7 +1702,14 @@ export function TableGrid({ cancelAnimationFrame(rafId) if (secondRaf) cancelAnimationFrame(secondRaf) } - }, [selectionAnchor, selectionFocus, isColumnSelection, rowVirtualizer, checkboxColWidth]) + }, [ + selectionAnchor, + selectionFocus, + isColumnSelection, + rowVirtualizer, + pinnedStickyLeftEdge, + pinnedColumnSet, + ]) const handleCellClick = useCallback( (rowId: string, columnName: string, options?: { toggleBoolean?: boolean }) => { @@ -2497,26 +2641,6 @@ export function TableGrid({ [] ) - const handleChangeType = useCallback((columnName: string, newType: ColumnDefinition['type']) => { - const column = columnsRef.current.find((c) => c.name === columnName) - const previousType = column?.type - updateColumnMutation.mutate( - { columnName, updates: { type: newType } }, - { - onSuccess: () => { - if (previousType) { - pushUndoRef.current({ - type: 'update-column-type', - columnName, - previousType, - newType, - }) - } - }, - } - ) - }, []) - const insertColumnInOrder = useCallback( (anchorColumn: string, newColumn: string, side: 'left' | 'right') => { const order = columnOrderRef.current ?? schemaColumnsRef.current.map((c) => c.name) @@ -2725,6 +2849,7 @@ export function TableGrid({ .map((r) => ({ rowId: r.id, value: r.data[columnToDelete] })) const previousWidth = columnWidthsRef.current[columnToDelete] ?? null const orderSnapshot = currentOrder ? [...currentOrder] : null + const pinnedSnapshot = [...pinnedColumnsRef.current] const onDeleted = () => { deletedOriginalPositions.push(entry.position) @@ -2738,21 +2863,32 @@ export function TableGrid({ cellData, previousOrder: orderSnapshot, previousWidth, + previousPinnedColumns: pinnedSnapshot, }) const { [columnToDelete]: _removedWidth, ...cleanedWidths } = columnWidthsRef.current setColumnWidths(cleanedWidths) columnWidthsRef.current = cleanedWidths + const updatedPinned = pinnedColumnsRef.current.filter((n) => n !== columnToDelete) + if (updatedPinned.length !== pinnedColumnsRef.current.length) { + setPinnedColumns(updatedPinned) + pinnedColumnsRef.current = updatedPinned + } + if (currentOrder) { currentOrder = currentOrder.filter((n) => n !== columnToDelete) setColumnOrder(currentOrder) updateMetadataRef.current({ columnWidths: cleanedWidths, columnOrder: currentOrder, + pinnedColumns: pinnedColumnsRef.current, }) } else { - updateMetadataRef.current({ columnWidths: cleanedWidths }) + updateMetadataRef.current({ + columnWidths: cleanedWidths, + pinnedColumns: pinnedColumnsRef.current, + }) } deleteNext(index + 1) @@ -2949,8 +3085,12 @@ export function TableGrid({ rowId: row.id, groupId, executionId: exec?.executionId ?? null, + // Requires a real executionId: an error that never produced an execution + // (e.g. enqueue failure → status 'error' with executionId null) has no + // trace to open, so "View execution" must not offer it. canViewExecution: !isEnrichmentGroup && + Boolean(exec?.executionId) && (status === 'completed' || status === 'error' || status === 'running' || isPaused), } }, [normalizedSelection, rows, displayColumns, workflowGroupById]) @@ -3097,16 +3237,6 @@ export function TableGrid({ return (
    - {embedded && totalRunning > 0 && ( -
    - -
    - )} -
    - {headerGroups.map((g) => - g.kind === 'workflow' ? ( - = g.startColIndex + g.size - 1 - } - groupId={g.groupId} - groupType={workflowGroupById.get(g.groupId)?.type} - enrichmentId={workflowGroupById.get(g.groupId)?.enrichmentId} - groupName={workflowGroupById.get(g.groupId)?.name} - onSelectGroup={handleGroupSelect} - onOpenConfig={() => handleConfigureWorkflowGroup(g.groupId)} - onRunColumn={userPermissions.canEdit ? handleRunColumn : undefined} - selectedRowIds={selectedRowIds} - onInsertLeft={ - userPermissions.canEdit ? handleInsertColumnLeft : undefined - } - onInsertRight={ - userPermissions.canEdit ? handleInsertColumnRight : undefined - } - onDeleteColumn={ - userPermissions.canEdit ? handleDeleteColumn : undefined - } - onDeleteGroup={ - userPermissions.canEdit ? handleDeleteWorkflowGroup : undefined - } - onViewWorkflow={ - workflowGroupById.get(g.groupId)?.type === 'enrichment' - ? undefined - : handleViewWorkflow - } - readOnly={!userPermissions.canEdit} - onDragStart={ - userPermissions.canEdit ? handleColumnDragStart : undefined - } - onDragOver={ - userPermissions.canEdit ? handleColumnDragOver : undefined - } - onDragEnd={userPermissions.canEdit ? handleColumnDragEnd : undefined} - onDragLeave={ - userPermissions.canEdit ? handleColumnDragLeave : undefined - } - /> - ) : ( + {headerGroups.map((g) => { + const firstCol = displayColumns[g.startColIndex] + const stickyLeft = firstCol ? pinnedOffsets.get(firstCol.key) : undefined + if (g.kind === 'workflow') { + const lastCol = displayColumns[g.startColIndex + g.size - 1] + return ( + = g.startColIndex + g.size - 1 + } + groupId={g.groupId} + groupType={workflowGroupById.get(g.groupId)?.type} + enrichmentId={workflowGroupById.get(g.groupId)?.enrichmentId} + groupName={workflowGroupById.get(g.groupId)?.name} + onSelectGroup={handleGroupSelect} + onOpenConfig={() => handleConfigureWorkflowGroup(g.groupId)} + onRunColumn={userPermissions.canEdit ? handleRunColumn : undefined} + selectedRowIds={selectedRowIds} + onInsertLeft={ + userPermissions.canEdit ? handleInsertColumnLeft : undefined + } + onInsertRight={ + userPermissions.canEdit ? handleInsertColumnRight : undefined + } + onDeleteColumn={ + userPermissions.canEdit ? handleDeleteColumn : undefined + } + onDeleteGroup={ + userPermissions.canEdit ? handleDeleteWorkflowGroup : undefined + } + onViewWorkflow={ + workflowGroupById.get(g.groupId)?.type === 'enrichment' + ? undefined + : handleViewWorkflow + } + readOnly={!userPermissions.canEdit} + onDragStart={ + userPermissions.canEdit ? handleColumnDragStart : undefined + } + onDragOver={ + userPermissions.canEdit ? handleColumnDragOver : undefined + } + onDragEnd={ + userPermissions.canEdit ? handleColumnDragEnd : undefined + } + onDragLeave={ + userPermissions.canEdit ? handleColumnDragLeave : undefined + } + isPinned={firstCol ? pinnedColumnSet.has(firstCol.name) : false} + onPinToggle={userPermissions.canEdit ? handlePinToggle : undefined} + stickyLeft={stickyLeft} + isLastPinned={lastCol?.key === lastPinnedColKey} + /> + ) + } + const isLastFrz = firstCol?.key === lastPinnedColKey + return ( ) - )} + })} {userPermissions.canEdit && ( )} @@ -3243,45 +3395,52 @@ export function TableGrid({ checked={isAllRowsSelected} onCheckedChange={handleSelectAllToggle} /> - {displayColumns.map((column, idx) => ( - = normalizedSelection.startCol && - idx <= normalizedSelection.endCol - } - renameValue={ - columnRename.editingId === column.name ? columnRename.editValue : '' - } - onRenameValueChange={columnRename.setEditValue} - onRenameSubmit={columnRename.submitRename} - onRenameCancel={columnRename.cancelRename} - onColumnSelect={handleColumnSelect} - onChangeType={handleChangeType} - onInsertLeft={handleInsertColumnLeft} - onInsertRight={handleInsertColumnRight} - onDeleteColumn={handleDeleteColumn} - onResizeStart={handleColumnResizeStart} - onResize={handleColumnResize} - onResizeEnd={handleColumnResizeEnd} - onAutoResize={handleColumnAutoResize} - onDragStart={handleColumnDragStart} - onDragOver={handleColumnDragOver} - onDragEnd={handleColumnDragEnd} - onDragLeave={handleColumnDragLeave} - workflows={workflows} - workflowGroups={tableWorkflowGroups} - sourceInfo={columnSourceInfo.get(column.name)} - onOpenConfig={handleConfigureColumn} - onViewWorkflow={handleViewWorkflow} - /> - ))} + {displayColumns.map((column, idx) => { + const colIsPinned = pinnedColumnSet.has(column.name) + const colStickyLeft = pinnedOffsets.get(column.key) + return ( + = normalizedSelection.startCol && + idx <= normalizedSelection.endCol + } + renameValue={ + columnRename.editingId === column.name ? columnRename.editValue : '' + } + onRenameValueChange={columnRename.setEditValue} + onRenameSubmit={columnRename.submitRename} + onRenameCancel={columnRename.cancelRename} + onColumnSelect={handleColumnSelect} + onInsertLeft={handleInsertColumnLeft} + onInsertRight={handleInsertColumnRight} + onDeleteColumn={handleDeleteColumn} + onResizeStart={handleColumnResizeStart} + onResize={handleColumnResize} + onResizeEnd={handleColumnResizeEnd} + onAutoResize={handleColumnAutoResize} + onDragStart={handleColumnDragStart} + onDragOver={handleColumnDragOver} + onDragEnd={handleColumnDragEnd} + onDragLeave={handleColumnDragLeave} + workflows={workflows} + workflowGroups={tableWorkflowGroups} + sourceInfo={columnSourceInfo.get(column.name)} + onOpenConfig={handleConfigureColumn} + onViewWorkflow={handleViewWorkflow} + isPinned={colIsPinned} + onPinToggle={userPermissions.canEdit ? handlePinToggle : undefined} + stickyLeft={colStickyLeft} + isLastPinned={column.key === lastPinnedColKey} + /> + ) + })} {userPermissions.canEdit && ( 0 ? pinnedOffsets : undefined} + lastPinnedColKey={lastPinnedColKey} /> ) })} @@ -3372,6 +3534,18 @@ export function TableGrid({ /> )} + {isFetchingNextPage && ( + + +
    + +
    + + + )} ) })() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts index d66182b70f4..6a7f53b2185 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts @@ -49,7 +49,10 @@ export function checkboxColLayout( ): { colWidth: number; numDivWidth: number } { const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1 const numDivWidth = Math.max(20, digits * 8 + 4) - const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0) + // When workflow columns are present a 20px run/stop button sits to the right of + // the number, separated by a 6px gap and a 4px right pad — 30px total. Reserving + // only the button width clipped the number on tables with many (wide) row indices. + const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 30 : 0) return { colWidth, numDivWidth } } @@ -196,6 +199,12 @@ export function resolveCellExec( // cell SSE) cover the actual rows instead. if (d.limit) continue if (!d.scope.groupIds.includes(group.id)) continue + // Auto-fire dispatches (row writes / schema changes) scope every group but + // the dispatcher honors `autoRun: false` per-cell ('autoRun-off'), so those + // cells never actually run — don't optimistically paint them Queued. Manual + // runs (Run all / Run column) bypass autoRun and DO run them, so keep the + // overlay's Queued there. + if (!d.isManualRun && group.autoRun === false) continue if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue if (row.position <= d.cursor) continue return { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index 4785764171a..f36cb0ac0ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -214,8 +214,8 @@ export function WorkflowSidebar(props: WorkflowSidebarProps) { role='dialog' aria-label='Configure workflow' className={cn( - 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out', - open ? 'translate-x-0' : 'translate-x-full' + 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] transition-transform duration-200 ease-out', + open ? 'translate-x-0 shadow-overlay' : 'translate-x-full' )} > {props.config && ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 64fa3c1af0d..466835d041b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -462,36 +462,46 @@ export function Table({ return (
    {!embedded && ( - <> - 0 ? ( - - ) : null - } - /> - setFilterOpen((prev) => !prev)} - filterActive={filterOpen || !!queryOptions.filter} - /> - {filterOpen && ( - setFilterOpen(false)} + 0 ? ( + + ) : null + } + /> + )} + {/* Sort + filter render in both modes. In embedded (mothership) mode there's + no ResourceHeader, so the run/stop control rides in the options bar's + `extras` slot — keeping the bar populated whether or not a run is live. */} + setFilterOpen((prev) => !prev)} + filterActive={filterOpen || !!queryOptions.filter} + extras={ + embedded && selection.totalRunning > 0 ? ( + - )} - + ) : undefined + } + /> + {filterOpen && ( + setFilterOpen(false)} + /> )} { @@ -160,9 +164,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model') { return opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -170,7 +174,7 @@ export const ComboBox = memo(function ComboBox({ } return opts - }, [options, subBlockId, isProviderAllowed]) + }, [options, subBlockId, isProviderAllowed, isModelAllowed]) // Normalize fetched options to match ComboBoxOption format const normalizedFetchedOptions = useMemo((): ComboBoxOption[] => { @@ -185,9 +189,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model' && fetchOptions && normalizedFetchedOptions.length > 0) { opts = opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -212,6 +216,7 @@ export const ComboBox = memo(function ComboBox({ hydratedOption, subBlockId, isProviderAllowed, + isModelAllowed, ]) // Convert options to Combobox format diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index 1baaa3b20b4..76c2333d9fd 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -26,6 +26,7 @@ import { RateLimiter } from '@/lib/core/rate-limiter' import { decryptSecret } from '@/lib/core/security/encryption' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { getBaseUrl } from '@/lib/core/utils/urls' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' import { sendEmail } from '@/lib/messaging/email/mailer' import type { AlertConfig } from '@/lib/notifications/alert-rules' @@ -507,27 +508,6 @@ function formatLogDate(value: Date | string | null | undefined, fallback = ''): return typeof value === 'string' ? value : fallback } -function normalizeLogCost(value: unknown): WorkflowExecutionLog['cost'] { - if (!isRecord(value)) { - return undefined - } - - const tokens = isRecord(value.tokens) - ? { - input: typeof value.tokens.input === 'number' ? value.tokens.input : undefined, - output: typeof value.tokens.output === 'number' ? value.tokens.output : undefined, - total: typeof value.tokens.total === 'number' ? value.tokens.total : undefined, - } - : undefined - - return { - input: typeof value.input === 'number' ? value.input : undefined, - output: typeof value.output === 'number' ? value.output : undefined, - total: typeof value.total === 'number' ? value.total : undefined, - tokens, - } -} - function normalizeLogFiles(value: unknown): WorkflowExecutionLog['files'] { if (!Array.isArray(value)) { return undefined @@ -545,11 +525,18 @@ function normalizeLogFiles(value: unknown): WorkflowExecutionLog['files'] { ) } -function normalizeWorkflowExecutionLog( +async function normalizeWorkflowExecutionLog( row: typeof workflowExecutionLogs.$inferSelect -): WorkflowExecutionLog { +): Promise { const startedAt = formatLogDate(row.startedAt) + // Heavy execution data may live in object storage; resolve the pointer so + // retry deliveries get finalOutput/traceSpans (no-op for inline rows). + const executionData = await materializeExecutionData( + isRecord(row.executionData) ? row.executionData : {}, + { workspaceId: row.workspaceId, workflowId: row.workflowId, executionId: row.executionId } + ) + return { id: row.id, workflowId: row.workflowId, @@ -561,8 +548,9 @@ function normalizeWorkflowExecutionLog( endedAt: formatLogDate(row.endedAt, startedAt), totalDurationMs: row.totalDurationMs ?? 0, files: normalizeLogFiles(row.files), - executionData: isRecord(row.executionData) ? row.executionData : {}, - cost: normalizeLogCost(row.cost), + executionData: executionData as WorkflowExecutionLog['executionData'], + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: row.costTotal != null ? { total: Number(row.costTotal) } : undefined, createdAt: formatLogDate(row.createdAt, startedAt), } } @@ -580,7 +568,7 @@ async function buildRetryLog(params: NotificationDeliveryParams): Promise = { placeholder: 'e.g. React, TypeScript, Node.js', }, }, - // API Key + // API Key — hidden on hosted Sim for operations with hosted-key support { id: 'apiKey', title: 'API Key', @@ -222,6 +222,18 @@ export const FindymailBlock: BlockConfig = { required: true, placeholder: 'Enter your Findymail API key', password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'findymail_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Findymail API key', + password: true, + condition: { field: 'operation', value: 'findymail_get_credits' }, }, ], tools: { diff --git a/apps/sim/blocks/blocks/prospeo.ts b/apps/sim/blocks/blocks/prospeo.ts index 2980efdb96a..b28fc3417a2 100644 --- a/apps/sim/blocks/blocks/prospeo.ts +++ b/apps/sim/blocks/blocks/prospeo.ts @@ -302,7 +302,7 @@ export const ProspeoBlock: BlockConfig = { condition: { field: 'operation', value: 'prospeo_search_suggestions' }, }, - // API Key (always last) + // API Key — hidden on hosted Sim for operations with hosted-key support { id: 'apiKey', title: 'API Key', @@ -310,6 +310,25 @@ export const ProspeoBlock: BlockConfig = { required: true, placeholder: 'Enter your Prospeo API key', password: true, + hideWhenHosted: true, + condition: { + field: 'operation', + value: ['prospeo_search_suggestions', 'prospeo_account_information'], + not: true, + }, + }, + // API Key — always required for the free account/suggestion lookups (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Prospeo API key', + password: true, + condition: { + field: 'operation', + value: ['prospeo_search_suggestions', 'prospeo_account_information'], + }, }, ], tools: { diff --git a/apps/sim/blocks/blocks/wiza.ts b/apps/sim/blocks/blocks/wiza.ts index 91a1acdff89..fe648c0f7a9 100644 --- a/apps/sim/blocks/blocks/wiza.ts +++ b/apps/sim/blocks/blocks/wiza.ts @@ -24,8 +24,7 @@ export const WizaBlock: BlockConfig = { options: [ { label: 'Prospect Search', id: 'prospect_search' }, { label: 'Company Enrichment', id: 'company_enrichment' }, - { label: 'Start Individual Reveal', id: 'start_individual_reveal' }, - { label: 'Get Individual Reveal', id: 'get_individual_reveal' }, + { label: 'Individual Reveal', id: 'individual_reveal' }, { label: 'Get Credits', id: 'get_credits' }, ], value: () => 'prospect_search', @@ -37,6 +36,17 @@ export const WizaBlock: BlockConfig = { placeholder: 'Enter your Wiza API key', password: true, required: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'get_credits', not: true }, + }, + { + id: 'apiKey', + title: 'Wiza API Key', + type: 'short-input', + placeholder: 'Enter your Wiza API key', + password: true, + required: true, + condition: { field: 'operation', value: 'get_credits' }, }, // Prospect Search @@ -287,7 +297,7 @@ Return ONLY the JSON object - no explanations, no extra text.`, mode: 'advanced', }, - // Start Individual Reveal + // Individual Reveal { id: 'enrichment_level', title: 'Enrichment Level', @@ -298,85 +308,66 @@ Return ONLY the JSON object - no explanations, no extra text.`, { label: 'Phone', id: 'phone' }, { label: 'Full', id: 'full' }, ], - value: () => 'partial', - condition: { field: 'operation', value: 'start_individual_reveal' }, - required: { field: 'operation', value: 'start_individual_reveal' }, + value: () => 'full', + condition: { field: 'operation', value: 'individual_reveal' }, + required: { field: 'operation', value: 'individual_reveal' }, }, { id: 'profile_url', title: 'LinkedIn Profile URL', type: 'short-input', placeholder: 'https://linkedin.com/in/johndoe', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'full_name', title: 'Full Name', type: 'short-input', placeholder: 'John Doe', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'company', title: 'Company', type: 'short-input', placeholder: 'Wiza', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'domain', title: 'Company Domain', type: 'short-input', placeholder: 'wiza.co', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'email', title: 'Email', type: 'short-input', placeholder: 'john@wiza.co', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'accept_work', title: 'Accept Work Emails', type: 'switch', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, mode: 'advanced', }, { id: 'accept_personal', title: 'Accept Personal Emails', type: 'switch', - condition: { field: 'operation', value: 'start_individual_reveal' }, - mode: 'advanced', - }, - { - id: 'callback_url', - title: 'Callback URL', - type: 'short-input', - placeholder: 'https://example.com/wiza-callback', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, mode: 'advanced', }, - - // Get Individual Reveal - { - id: 'id', - title: 'Reveal ID', - type: 'short-input', - placeholder: 'Reveal ID returned from Start Individual Reveal', - condition: { field: 'operation', value: 'get_individual_reveal' }, - required: { field: 'operation', value: 'get_individual_reveal' }, - }, ], tools: { access: [ 'wiza_prospect_search', 'wiza_company_enrichment', - 'wiza_start_individual_reveal', - 'wiza_get_individual_reveal', + 'wiza_individual_reveal', 'wiza_get_credits', ], config: { @@ -386,10 +377,8 @@ Return ONLY the JSON object - no explanations, no extra text.`, return 'wiza_prospect_search' case 'company_enrichment': return 'wiza_company_enrichment' - case 'start_individual_reveal': - return 'wiza_start_individual_reveal' - case 'get_individual_reveal': - return 'wiza_get_individual_reveal' + case 'individual_reveal': + return 'wiza_individual_reveal' case 'get_credits': return 'wiza_get_credits' default: @@ -479,8 +468,6 @@ Return ONLY the JSON object - no explanations, no extra text.`, email: { type: 'string', description: 'Email address' }, accept_work: { type: 'boolean', description: 'Whether to accept work emails' }, accept_personal: { type: 'boolean', description: 'Whether to accept personal emails' }, - callback_url: { type: 'string', description: 'Callback URL' }, - id: { type: 'string', description: 'Individual reveal ID' }, }, outputs: { @@ -495,46 +482,44 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, id: { type: 'number', - description: 'Reveal ID (start_individual_reveal, get_individual_reveal)', + description: 'Reveal ID (individual_reveal)', }, status: { type: 'string', - description: - 'Reveal status (start_individual_reveal, get_individual_reveal): queued | resolving | finished | failed', + description: 'Reveal status (individual_reveal): queued | resolving | finished | failed', }, is_complete: { type: 'boolean', - description: - 'Whether the reveal has completed (start_individual_reveal, get_individual_reveal)', + description: 'Whether the reveal has completed (individual_reveal)', }, - name: { type: 'string', description: 'Full name (get_individual_reveal)' }, - company: { type: 'string', description: 'Company name (get_individual_reveal)' }, + name: { type: 'string', description: 'Full name (individual_reveal)' }, + company: { type: 'string', description: 'Company name (individual_reveal)' }, enrichment_level: { type: 'string', - description: 'Enrichment level used (get_individual_reveal)', + description: 'Enrichment level used (individual_reveal)', }, - linkedin_profile_url: { type: 'string', description: 'LinkedIn URL (get_individual_reveal)' }, - title: { type: 'string', description: 'Job title (get_individual_reveal)' }, - location: { type: 'string', description: 'Location (get_individual_reveal)' }, - email: { type: 'string', description: 'Primary email (get_individual_reveal)' }, - email_type: { type: 'string', description: 'Primary email type (get_individual_reveal)' }, + linkedin_profile_url: { type: 'string', description: 'LinkedIn URL (individual_reveal)' }, + title: { type: 'string', description: 'Job title (individual_reveal)' }, + location: { type: 'string', description: 'Location (individual_reveal)' }, + email: { type: 'string', description: 'Primary email (individual_reveal)' }, + email_type: { type: 'string', description: 'Primary email type (individual_reveal)' }, email_status: { type: 'string', - description: 'Primary email status: valid | risky | unfound (get_individual_reveal)', + description: 'Primary email status: valid | risky | unfound (individual_reveal)', }, emails: { type: 'json', - description: 'All emails found (get_individual_reveal): [{email, email_type, email_status}]', + description: 'All emails found (individual_reveal): [{email, email_type, email_status}]', }, - mobile_phone: { type: 'string', description: 'Mobile phone (get_individual_reveal)' }, - phone_number: { type: 'string', description: 'Direct/office phone (get_individual_reveal)' }, + mobile_phone: { type: 'string', description: 'Mobile phone (individual_reveal)' }, + phone_number: { type: 'string', description: 'Direct/office phone (individual_reveal)' }, phone_status: { type: 'string', - description: 'Phone status: found | unfound (get_individual_reveal)', + description: 'Phone status: found | unfound (individual_reveal)', }, phones: { type: 'json', - description: 'All phones found (get_individual_reveal): [{number, pretty_number, type}]', + description: 'All phones found (individual_reveal): [{number, pretty_number, type}]', }, company_name: { type: 'string', @@ -542,41 +527,41 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, company_domain: { type: 'string', - description: 'Company domain (company_enrichment, get_individual_reveal)', + description: 'Company domain (company_enrichment, individual_reveal)', }, domain: { type: 'string', description: 'Domain (company_enrichment)' }, company_industry: { type: 'string', - description: 'Industry (company_enrichment, get_individual_reveal)', + description: 'Industry (company_enrichment, individual_reveal)', }, company_size: { type: 'number', - description: 'Employee count (company_enrichment, get_individual_reveal)', + description: 'Employee count (company_enrichment, individual_reveal)', }, company_size_range: { type: 'string', - description: 'Headcount range (company_enrichment, get_individual_reveal)', + description: 'Headcount range (company_enrichment, individual_reveal)', }, company_founded: { type: 'number', - description: 'Year founded (company_enrichment, get_individual_reveal)', + description: 'Year founded (company_enrichment, individual_reveal)', }, company_revenue_range: { type: 'string', description: 'Revenue range (company_enrichment)', }, - company_revenue: { type: 'string', description: 'Revenue (get_individual_reveal)' }, + company_revenue: { type: 'string', description: 'Revenue (individual_reveal)' }, company_funding: { type: 'string', - description: 'Total funding (company_enrichment, get_individual_reveal)', + description: 'Total funding (company_enrichment, individual_reveal)', }, company_type: { type: 'string', - description: 'Company type (company_enrichment, get_individual_reveal)', + description: 'Company type (company_enrichment, individual_reveal)', }, company_description: { type: 'string', - description: 'Company description (company_enrichment, get_individual_reveal)', + description: 'Company description (company_enrichment, individual_reveal)', }, company_ticker: { type: 'string', description: 'Stock ticker (company_enrichment)' }, company_last_funding_round: { @@ -593,40 +578,40 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, company_location: { type: 'string', - description: 'Full location string (company_enrichment, get_individual_reveal)', + description: 'Full location string (company_enrichment, individual_reveal)', }, company_twitter: { type: 'string', description: 'Twitter URL (company_enrichment)' }, company_facebook: { type: 'string', description: 'Facebook URL (company_enrichment)' }, company_linkedin: { type: 'string', - description: 'LinkedIn URL (company_enrichment, get_individual_reveal)', + description: 'LinkedIn URL (company_enrichment, individual_reveal)', }, company_linkedin_id: { type: 'string', description: 'LinkedIn ID (company_enrichment)' }, company_street: { type: 'string', - description: 'Street address (company_enrichment, get_individual_reveal)', + description: 'Street address (company_enrichment, individual_reveal)', }, company_locality: { type: 'string', - description: 'City (company_enrichment, get_individual_reveal)', + description: 'City (company_enrichment, individual_reveal)', }, company_region: { type: 'string', - description: 'State/region (company_enrichment, get_individual_reveal)', + description: 'State/region (company_enrichment, individual_reveal)', }, company_postal_code: { type: 'string', - description: 'Postal code (company_enrichment, get_individual_reveal)', + description: 'Postal code (company_enrichment, individual_reveal)', }, company_country: { type: 'string', - description: 'Country (company_enrichment, get_individual_reveal)', + description: 'Country (company_enrichment, individual_reveal)', }, - company_subindustry: { type: 'string', description: 'Subindustry (get_individual_reveal)' }, + company_subindustry: { type: 'string', description: 'Subindustry (individual_reveal)' }, credits: { type: 'json', description: - 'Credits deducted — company_enrichment: { api_credits: { total, company_credits } }; get_individual_reveal: { api_credits: { total, email_credits, phone_credits, scrape_credits } }', + 'Credits deducted — company_enrichment: { api_credits: { total, company_credits } }; individual_reveal: { api_credits: { total, email_credits, phone_credits, scrape_credits } }', }, email_credits: { type: 'json', diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 83c087a599e..3d1cb1fdaf6 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -60,6 +60,8 @@ export { PanelLeft } from './panel-left' export { Pause } from './pause' export { Pencil } from './pencil' export { PillsRing } from './pills-ring' +export { Pin } from './pin' +export { PinOff } from './pin-off' export { Play, PlayOutline } from './play' export { Plus } from './plus' export { Redo } from './redo' diff --git a/apps/sim/components/emcn/icons/pin-off.tsx b/apps/sim/components/emcn/icons/pin-off.tsx new file mode 100644 index 00000000000..0f1bf606275 --- /dev/null +++ b/apps/sim/components/emcn/icons/pin-off.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * PinOff icon component - thumbtack pin with diagonal strike-through + * @param props - SVG properties including className, fill, etc. + */ +export function PinOff(props: SVGProps) { + return ( + + ) +} diff --git a/apps/sim/components/emcn/icons/pin.tsx b/apps/sim/components/emcn/icons/pin.tsx new file mode 100644 index 00000000000..0e9fbfec2a0 --- /dev/null +++ b/apps/sim/components/emcn/icons/pin.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Pin icon component - thumbtack pin + * @param props - SVG properties including className, fill, etc. + */ +export function Pin(props: SVGProps) { + return ( + + ) +} diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 930c9da176b..30f744225d6 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { Plus, Search } from 'lucide-react' +import { ChevronDown, Plus, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { Avatar, @@ -27,6 +27,7 @@ import { } from '@/components/emcn' import { Input as BaseInput } from '@/components/ui' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { cn } from '@/lib/core/utils/cn' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getUserColor } from '@/lib/workspaces/colors' import { getAllBlocks } from '@/blocks' @@ -42,9 +43,16 @@ import { useUserPermissionConfig, } from '@/ee/access-control/hooks/permission-groups' import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' +import { useProviderModels } from '@/hooks/queries/providers' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' -import { PROVIDER_DEFINITIONS } from '@/providers/models' -import { getAllProviderIds } from '@/providers/utils' +import { + DYNAMIC_MODEL_PROVIDERS, + getProviderModels, + PROVIDER_DEFINITIONS, +} from '@/providers/models' +import type { ProviderId } from '@/providers/types' +import { getAllProviderIds, getProviderFromModel } from '@/providers/utils' +import type { ProviderName } from '@/stores/providers' const logger = createLogger('AccessControl') @@ -252,6 +260,188 @@ function AccessControlSkeleton() { ) } +interface ModelDenylistControls { + isModelAllowed: (model: string) => boolean + onToggleModel: (model: string) => void + onSetModelsDenied: (models: string[], denied: boolean) => void +} + +interface ModelCheckboxGridProps extends ModelDenylistControls { + models: string[] + isLoading: boolean +} + +function ModelCheckboxGrid({ + models, + isLoading, + isModelAllowed, + onToggleModel, + onSetModelsDenied, +}: ModelCheckboxGridProps) { + const [search, setSearch] = useState('') + + const sortedModels = useMemo(() => [...models].sort((a, b) => a.localeCompare(b)), [models]) + + const filteredModels = useMemo(() => { + if (!search.trim()) return sortedModels + const query = search.toLowerCase() + return sortedModels.filter((model) => model.toLowerCase().includes(query)) + }, [sortedModels, search]) + + if (isLoading) { + return
    Loading models…
    + } + + if (models.length === 0) { + return ( +
    + No models available for this provider. +
    + ) + } + + const allFilteredAllowed = filteredModels.every((model) => isModelAllowed(model)) + + return ( +
    +
    +
    + + setSearch(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
    + +
    +
    + {filteredModels.map((model) => { + const checkboxId = `model-${model}` + return ( + + ) + })} +
    +
    + ) +} + +interface DynamicProviderModelsProps extends ModelDenylistControls { + provider: ProviderName + workspaceId?: string +} + +function DynamicProviderModels({ provider, workspaceId, ...controls }: DynamicProviderModelsProps) { + const { data, isPending } = useProviderModels(provider, workspaceId) + return +} + +interface StaticProviderModelsProps extends ModelDenylistControls { + providerId: ProviderId +} + +function StaticProviderModels({ providerId, ...controls }: StaticProviderModelsProps) { + const models = useMemo(() => getProviderModels(providerId), [providerId]) + return +} + +interface ProviderRowProps extends ModelDenylistControls { + providerId: ProviderId + isProviderAllowed: boolean + onToggleProvider: () => void + deniedCount: number + workspaceId?: string +} + +function ProviderRow({ + providerId, + isProviderAllowed, + onToggleProvider, + deniedCount, + workspaceId, + ...controls +}: ProviderRowProps) { + const [expanded, setExpanded] = useState(false) + + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const isDynamic = (DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId) + const checkboxId = `provider-${providerId}` + + return ( +
    +
    + onToggleProvider()} + /> +
    + {ProviderIcon && } +
    + +
    + {expanded && isProviderAllowed && ( +
    + {isDynamic ? ( + + ) : ( + + )} +
    + )} +
    + ) +} + export function AccessControl() { const params = useParams() const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined @@ -748,6 +938,65 @@ export function AccessControl() { [editingConfig] ) + const isModelAllowed = useCallback( + (model: string) => { + if (!editingConfig) return true + const normalized = model.toLowerCase() + return !editingConfig.deniedModels.some((denied) => denied.toLowerCase() === normalized) + }, + [editingConfig] + ) + + const toggleModel = useCallback( + (model: string) => { + if (!editingConfig) return + const normalized = model.toLowerCase() + const isDenied = editingConfig.deniedModels.some( + (denied) => denied.toLowerCase() === normalized + ) + const deniedModels = isDenied + ? editingConfig.deniedModels.filter((denied) => denied.toLowerCase() !== normalized) + : [...editingConfig.deniedModels, model] + setEditingConfig({ ...editingConfig, deniedModels }) + }, + [editingConfig] + ) + + const setModelsDenied = useCallback( + (models: string[], denied: boolean) => { + if (!editingConfig) return + if (denied) { + const existing = new Set(editingConfig.deniedModels.map((m) => m.toLowerCase())) + const additions = models.filter((m) => !existing.has(m.toLowerCase())) + if (additions.length === 0) return + setEditingConfig({ + ...editingConfig, + deniedModels: [...editingConfig.deniedModels, ...additions], + }) + } else { + const toRemove = new Set(models.map((m) => m.toLowerCase())) + setEditingConfig({ + ...editingConfig, + deniedModels: editingConfig.deniedModels.filter((m) => !toRemove.has(m.toLowerCase())), + }) + } + }, + [editingConfig] + ) + + const deniedCountByProvider = useMemo(() => { + const counts: Record = {} + for (const model of editingConfig?.deniedModels ?? []) { + try { + const providerId = getProviderFromModel(model) + counts[providerId] = (counts[providerId] ?? 0) + 1 + } catch { + // Unknown/blacklisted provider — omit from counts. + } + } + return counts + }, [editingConfig?.deniedModels]) + const availableMembersToAdd = useMemo(() => { const existingMemberUserIds = new Set(members.map((m) => m.userId)) return workspaceMembers.filter((m) => !existingMemberUserIds.has(m.userId)) @@ -945,31 +1194,20 @@ export function AccessControl() { : 'Select All'}
    -
    - {filteredProviders.map((providerId) => { - const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon - const providerName = - PROVIDER_DEFINITIONS[providerId]?.name || - providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - const checkboxId = `provider-${providerId}` - return ( - - ) - })} +
    + {filteredProviders.map((providerId) => ( + toggleProvider(providerId)} + deniedCount={deniedCountByProvider[providerId] ?? 0} + workspaceId={workspaceId} + isModelAllowed={isModelAllowed} + onToggleModel={toggleModel} + onSetModelsDenied={setModelsDenied} + /> + ))}
    diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index 0c0a3934399..5a2cf46bced 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -13,6 +13,7 @@ const { DEFAULT_PERMISSION_GROUP_CONFIG: { allowedIntegrations: null, allowedModelProviders: null, + deniedModels: [], hideTraceSpans: false, hideKnowledgeBaseTab: false, hideTablesTab: false, @@ -94,6 +95,7 @@ import { getUserPermissionConfig, IntegrationNotAllowedError, McpToolsNotAllowedError, + ModelNotAllowedError, ProviderNotAllowedError, SkillsNotAllowedError, validateBlockType, @@ -237,6 +239,42 @@ describe('validateModelProvider', () => { await validateModelProvider('user-123', 'workspace-1', 'gpt-4') }) + + it('throws ModelNotAllowedError when the model is on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect(validateModelProvider('user-123', 'workspace-1', 'gpt-4')).rejects.toBeInstanceOf( + ModelNotAllowedError + ) + }) + + it('denylist match is case-insensitive', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['Ollama/Llama3'] } }] + mockGetProviderFromModel.mockReturnValue('ollama') + + await expect( + validateModelProvider('user-123', 'workspace-1', 'ollama/llama3') + ).rejects.toBeInstanceOf(ModelNotAllowedError) + }) + + it('enforces the denylist even when no provider allowlist is set', async () => { + mockDbGroupMembership.value = [ + { config: { allowedModelProviders: null, deniedModels: ['gpt-4'] } }, + ] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect(validateModelProvider('user-123', 'workspace-1', 'gpt-4')).rejects.toBeInstanceOf( + ModelNotAllowedError + ) + }) + + it('allows a model that is not on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await validateModelProvider('user-123', 'workspace-1', 'gpt-4o') + }) }) describe('validateMcpToolsAllowed', () => { @@ -281,6 +319,19 @@ describe('assertPermissionsAllowed', () => { ).rejects.toBeInstanceOf(ProviderNotAllowedError) }) + it('throws ModelNotAllowedError when the model is on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + model: 'gpt-4', + }) + ).rejects.toBeInstanceOf(ModelNotAllowedError) + }) + it('throws IntegrationNotAllowedError when block type is blocked', async () => { mockDbGroupMembership.value = [{ config: { allowedIntegrations: ['slack'] } }] diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index e871cf45740..400682953fe 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -29,6 +29,13 @@ export class ProviderNotAllowedError extends Error { } } +export class ModelNotAllowedError extends Error { + constructor(model: string) { + super(`Model "${model}" is not allowed based on your permission group settings`) + this.name = 'ModelNotAllowedError' + } +} + export class IntegrationNotAllowedError extends Error { constructor(blockType: string, reason?: string) { super( @@ -168,6 +175,18 @@ async function getPermissionConfig( return getUserPermissionConfig(userId, workspaceId) } +/** + * Returns true when `model` appears in the group's model denylist. Comparison is + * case-insensitive to match the normalization applied by `getProviderFromModel`. + */ +function isModelDenied(config: PermissionGroupConfig, model: string): boolean { + if (!config.deniedModels || config.deniedModels.length === 0) { + return false + } + const normalized = model.toLowerCase() + return config.deniedModels.some((denied) => denied.toLowerCase() === normalized) +} + export async function validateModelProvider( userId: string | undefined, workspaceId: string | undefined, @@ -180,20 +199,27 @@ export async function validateModelProvider( const config = await getPermissionConfig(userId, workspaceId, ctx) - if (!config || config.allowedModelProviders === null) { + if (!config) { return } - const providerId = getProviderFromModel(model) + if (config.allowedModelProviders !== null) { + const providerId = getProviderFromModel(model) - if (!config.allowedModelProviders.includes(providerId)) { - logger.warn('Model provider blocked by permission group', { - userId, - workspaceId, - model, - providerId, - }) - throw new ProviderNotAllowedError(providerId, model) + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) + throw new ProviderNotAllowedError(providerId, model) + } + } + + if (isModelDenied(config, model)) { + logger.warn('Model blocked by permission group', { userId, workspaceId, model }) + throw new ModelNotAllowedError(model) } } @@ -421,16 +447,23 @@ export async function assertPermissionsAllowed(req: PermissionAssertion): Promis ? await getPermissionConfig(userId, workspaceId, ctx) : mergeEnvAllowlist(null) - if (model && config && config.allowedModelProviders !== null) { - const providerId = getProviderFromModel(model) - if (!config.allowedModelProviders.includes(providerId)) { - logger.warn('Model provider blocked by permission group', { - userId, - workspaceId, - model, - providerId, - }) - throw new ProviderNotAllowedError(providerId, model) + if (model && config) { + if (config.allowedModelProviders !== null) { + const providerId = getProviderFromModel(model) + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) + throw new ProviderNotAllowedError(providerId, model) + } + } + + if (isModelDenied(config, model)) { + logger.warn('Model blocked by permission group', { userId, workspaceId, model }) + throw new ModelNotAllowedError(model) } } diff --git a/apps/sim/enrichments/phone-number/phone-number.test.ts b/apps/sim/enrichments/phone-number/phone-number.test.ts new file mode 100644 index 00000000000..2aa82c33eed --- /dev/null +++ b/apps/sim/enrichments/phone-number/phone-number.test.ts @@ -0,0 +1,78 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { phoneNumberEnrichment } from '@/enrichments/phone-number/phone-number' +import type { EnrichmentProvider } from '@/enrichments/types' + +function provider(id: string): EnrichmentProvider { + const p = phoneNumberEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in phone-number cascade`) + return p +} + +const nameDomain = { fullName: 'John Doe', companyDomain: 'https://www.acme.com/careers' } +const linkedinOnly = { fullName: 'John Doe', linkedinUrl: 'https://linkedin.com/in/johndoe' } + +describe('phone-number enrichment cascade', () => { + it('chains PDL then the phone-capable hosted providers', () => { + expect(phoneNumberEnrichment.providers.map((p) => p.id)).toEqual([ + 'pdl', + 'wiza', + 'findymail', + 'prospeo', + ]) + }) + + describe('wiza (opportunistic)', () => { + const p = provider('wiza') + it('reveals phone, using name+domain or LinkedIn profile_url', () => { + expect(p.toolId).toBe('wiza_individual_reveal') + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + domain: 'acme.com', + enrichment_level: 'phone', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + profile_url: 'https://linkedin.com/in/johndoe', + enrichment_level: 'phone', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ mobile_phone: '+1555', phones: [] })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ phones: [{ number: '+1777' }] })).toEqual({ phone: '+1777' }) + }) + }) + + describe('findymail', () => { + const p = provider('findymail') + it('keys off the LinkedIn URL and skips without one', () => { + expect(p.toolId).toBe('findymail_find_phone') + expect(p.buildParams(linkedinOnly)).toEqual({ + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ phone: '+1555' })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ phone: null })).toBeNull() + }) + }) + + describe('prospeo (opportunistic)', () => { + const p = provider('prospeo') + it('requests mobile enrichment via name+domain or LinkedIn', () => { + expect(p.toolId).toBe('prospeo_enrich_person') + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + company_website: 'acme.com', + enrich_mobile: true, + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + linkedin_url: 'https://linkedin.com/in/johndoe', + enrich_mobile: true, + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ person: { mobile: { mobile: '+1555' } } })).toEqual({ phone: '+1555' }) + }) + }) +}) diff --git a/apps/sim/enrichments/phone-number/phone-number.ts b/apps/sim/enrichments/phone-number/phone-number.ts index 5b76e12e806..6a7ad186cdd 100644 --- a/apps/sim/enrichments/phone-number/phone-number.ts +++ b/apps/sim/enrichments/phone-number/phone-number.ts @@ -4,17 +4,22 @@ import { firstNonEmpty, normalizeDomain, str, toolProvider } from '@/enrichments import type { EnrichmentConfig } from '@/enrichments/types' /** - * Phone Number enrichment. Finds a contact's phone number from their full name - * and (optionally) company domain via a People Data Labs person match. + * Phone Number enrichment. Finds a contact's phone number from a full name plus + * any available identifiers (company domain, LinkedIn URL) via a waterfall: + * People Data Labs (name match) → Wiza reveal → Findymail (LinkedIn) → Prospeo + * mobile. Each provider opportunistically uses whatever identifiers the row + * provides and self-skips when it has none usable, so adding more inputs widens + * coverage without reordering. First phone wins; all providers support hosted keys. */ export const phoneNumberEnrichment: EnrichmentConfig = { id: 'phone-number', name: 'Phone Number', - description: "Find a contact's phone number from their name and company domain.", + description: "Find a contact's phone number from their name, company, or LinkedIn URL.", icon: Phone, inputs: [ { id: 'fullName', name: 'Full name', type: 'string', required: true }, { id: 'companyDomain', name: 'Company domain', type: 'string' }, + { id: 'linkedinUrl', name: 'LinkedIn URL', type: 'string' }, ], outputs: [{ id: 'phone', name: 'phone', type: 'string' }], providers: [ @@ -37,5 +42,69 @@ export const phoneNumberEnrichment: EnrichmentConfig = { return phone ? { phone } : null }, }), + toolProvider({ + id: 'wiza', + label: 'Wiza', + toolId: 'wiza_individual_reveal', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + // Needs a LinkedIn URL or a name+domain pair; skip otherwise. + if (!linkedin && !(fullName && domain)) return null + // 'phone' reveals the mobile number (5 credits). Prefer LinkedIn when present. + return filterUndefined({ + profile_url: linkedin || undefined, + full_name: fullName || undefined, + domain: domain || undefined, + enrichment_level: 'phone', + }) + }, + mapOutput: (output) => { + const phones = Array.isArray(output.phones) + ? (output.phones as Record[]) + : [] + const phone = str(output.mobile_phone) || str(output.phone_number) || str(phones[0]?.number) + return phone ? { phone } : null + }, + }), + toolProvider({ + id: 'findymail', + label: 'Findymail', + toolId: 'findymail_find_phone', + buildParams: (inputs) => { + // Findymail's phone finder keys off a LinkedIn URL only. + const linkedin = str(inputs.linkedinUrl) + if (!linkedin) return null + return { linkedin_url: linkedin } + }, + mapOutput: (output) => { + const phone = str(output.phone) + return phone ? { phone } : null + }, + }), + toolProvider({ + id: 'prospeo', + label: 'Prospeo', + toolId: 'prospeo_enrich_person', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const companyWebsite = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && companyWebsite)) return null + return filterUndefined({ + linkedin_url: linkedin || undefined, + full_name: fullName || undefined, + company_website: companyWebsite || undefined, + enrich_mobile: true, + }) + }, + mapOutput: (output) => { + const person = output.person as Record | undefined + const mobile = person?.mobile as Record | undefined + const phone = str(mobile?.mobile) + return phone ? { phone } : null + }, + }), ], } diff --git a/apps/sim/enrichments/work-email/work-email.test.ts b/apps/sim/enrichments/work-email/work-email.test.ts new file mode 100644 index 00000000000..41bf50bc231 --- /dev/null +++ b/apps/sim/enrichments/work-email/work-email.test.ts @@ -0,0 +1,86 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { EnrichmentProvider } from '@/enrichments/types' +import { workEmailEnrichment } from '@/enrichments/work-email/work-email' + +function provider(id: string): EnrichmentProvider { + const p = workEmailEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in work-email cascade`) + return p +} + +const nameDomain = { fullName: 'John Doe', companyDomain: 'https://www.acme.com/careers' } +const linkedinOnly = { fullName: 'John Doe', linkedinUrl: 'https://linkedin.com/in/johndoe' } + +describe('work-email enrichment cascade', () => { + it('chains the hosted providers in waterfall order', () => { + expect(workEmailEnrichment.providers.map((p) => p.id)).toEqual([ + 'hunter', + 'findymail', + 'findymail-linkedin', + 'prospeo', + 'wiza', + 'pdl', + ]) + }) + + describe('findymail (name)', () => { + const p = provider('findymail') + it('maps name + domain and extracts contact.email', () => { + expect(p.toolId).toBe('findymail_find_email_from_name') + expect(p.buildParams(nameDomain)).toEqual({ name: 'John Doe', domain: 'acme.com' }) + expect(p.mapOutput({ contact: { email: 'j@acme.com' } })).toEqual({ email: 'j@acme.com' }) + expect(p.buildParams(linkedinOnly)).toBeNull() + }) + }) + + describe('findymail-linkedin', () => { + const p = provider('findymail-linkedin') + it('keys off the LinkedIn URL and skips without one', () => { + expect(p.toolId).toBe('findymail_find_email_from_linkedin') + expect(p.buildParams(linkedinOnly)).toEqual({ + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ contact: { email: 'j@acme.com' } })).toEqual({ email: 'j@acme.com' }) + }) + }) + + describe('prospeo (opportunistic)', () => { + const p = provider('prospeo') + it('uses name+domain, or LinkedIn when present', () => { + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + company_website: 'acme.com', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ person: { email: { email: 'j@acme.com' } } })).toEqual({ + email: 'j@acme.com', + }) + }) + }) + + describe('wiza (opportunistic)', () => { + const p = provider('wiza') + it('reveals email-only (partial), preferring LinkedIn profile_url', () => { + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + domain: 'acme.com', + enrichment_level: 'partial', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + profile_url: 'https://linkedin.com/in/johndoe', + enrichment_level: 'partial', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + }) + }) +}) diff --git a/apps/sim/enrichments/work-email/work-email.ts b/apps/sim/enrichments/work-email/work-email.ts index 0e998406c3a..2076e18eb46 100644 --- a/apps/sim/enrichments/work-email/work-email.ts +++ b/apps/sim/enrichments/work-email/work-email.ts @@ -1,21 +1,26 @@ import { filterUndefined } from '@sim/utils/object' -import { Mail } from 'lucide-react' +import { Mail } from '@/components/emcn/icons' import { normalizeDomain, splitName, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' /** - * Work Email enrichment. Finds a person's work email from their full name and - * company domain, trying Hunter first (deterministic finder) then People Data - * Labs (record match) as a fallback. + * Work Email enrichment. Finds a person's work email from a full name plus any + * available identifiers (company domain, LinkedIn URL) via a provider waterfall: + * deterministic finders first (Hunter, Findymail by name then by LinkedIn), then + * enrichment/reveal providers (Prospeo, Wiza), then People Data Labs as a broad + * record-match fallback. Each provider opportunistically uses whatever + * identifiers the row provides and self-skips when it has none usable, so adding + * more inputs widens coverage. First email wins; all providers support hosted keys. */ export const workEmailEnrichment: EnrichmentConfig = { id: 'work-email', name: 'Work Email', - description: "Find a person's work email from their name and company domain.", + description: "Find a person's work email from their name, company, or LinkedIn URL.", icon: Mail, inputs: [ { id: 'fullName', name: 'Full name', type: 'string', required: true }, - { id: 'companyDomain', name: 'Company domain', type: 'string', required: true }, + { id: 'companyDomain', name: 'Company domain', type: 'string' }, + { id: 'linkedinUrl', name: 'LinkedIn URL', type: 'string' }, ], outputs: [{ id: 'email', name: 'email', type: 'string' }], providers: [ @@ -34,6 +39,81 @@ export const workEmailEnrichment: EnrichmentConfig = { return email ? { email } : null }, }), + toolProvider({ + id: 'findymail', + label: 'Findymail', + toolId: 'findymail_find_email_from_name', + buildParams: (inputs) => { + const name = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + if (!name || !domain) return null + return { name, domain } + }, + mapOutput: (output) => { + const contact = output.contact as Record | null + const email = str(contact?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'findymail-linkedin', + label: 'Findymail (LinkedIn)', + toolId: 'findymail_find_email_from_linkedin', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + if (!linkedin) return null + return { linkedin_url: linkedin } + }, + mapOutput: (output) => { + const contact = output.contact as Record | null + const email = str(contact?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'prospeo', + label: 'Prospeo', + toolId: 'prospeo_enrich_person', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const companyWebsite = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && companyWebsite)) return null + return filterUndefined({ + linkedin_url: linkedin || undefined, + full_name: fullName || undefined, + company_website: companyWebsite || undefined, + }) + }, + mapOutput: (output) => { + const person = output.person as Record | undefined + const emailObj = person?.email as Record | undefined + const email = str(emailObj?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'wiza', + label: 'Wiza', + toolId: 'wiza_individual_reveal', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && domain)) return null + // 'partial' reveals the email only (2 credits); avoids phone charges. + return filterUndefined({ + profile_url: linkedin || undefined, + full_name: fullName || undefined, + domain: domain || undefined, + enrichment_level: 'partial', + }) + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), toolProvider({ id: 'pdl', label: 'People Data Labs', diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 4eb1f4577c1..492c77286ec 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -98,11 +98,15 @@ async function fetchWorkspaceFiles( /** * Hook to fetch workspace files */ -export function useWorkspaceFiles(workspaceId: string, scope: WorkspaceFileQueryScope = 'active') { +export function useWorkspaceFiles( + workspaceId: string, + scope: WorkspaceFileQueryScope = 'active', + options?: { enabled?: boolean } +) { return useQuery({ queryKey: workspaceFilesKeys.list(workspaceId, scope), queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, scope, signal), - enabled: !!workspaceId, + enabled: !!workspaceId && (options?.enabled ?? true), staleTime: 30 * 1000, // 30 seconds - files can change frequently placeholderData: keepPreviousData, // Show cached data immediately }) diff --git a/apps/sim/hooks/use-permission-config.ts b/apps/sim/hooks/use-permission-config.ts index cc52003b49f..88ef67f7229 100644 --- a/apps/sim/hooks/use-permission-config.ts +++ b/apps/sim/hooks/use-permission-config.ts @@ -21,6 +21,7 @@ export interface PermissionConfigResult { filterProviders: (providerIds: string[]) => string[] isBlockAllowed: (blockType: string) => boolean isProviderAllowed: (providerId: string) => boolean + isModelAllowed: (model: string) => boolean isInvitationsDisabled: boolean isPublicApiDisabled: boolean } @@ -98,6 +99,14 @@ export function usePermissionConfig(): PermissionConfigResult { } }, [config.allowedModelProviders]) + const isModelAllowed = useMemo(() => { + return (model: string) => { + if (config.deniedModels.length === 0) return true + const normalized = model.toLowerCase() + return !config.deniedModels.some((denied) => denied.toLowerCase() === normalized) + } + }, [config.deniedModels]) + const filterBlocks = useMemo(() => { return (blocks: T[]): T[] => { if (mergedAllowedIntegrations === null) return blocks @@ -140,6 +149,7 @@ export function usePermissionConfig(): PermissionConfigResult { filterProviders, isBlockAllowed, isProviderAllowed, + isModelAllowed, isInvitationsDisabled, isPublicApiDisabled, }), @@ -151,6 +161,7 @@ export function usePermissionConfig(): PermissionConfigResult { filterProviders, isBlockAllowed, isProviderAllowed, + isModelAllowed, isInvitationsDisabled, isPublicApiDisabled, ] diff --git a/apps/sim/hooks/use-table-undo.test.ts b/apps/sim/hooks/use-table-undo.test.ts index 7a5e2db347d..3caee9dcca7 100644 --- a/apps/sim/hooks/use-table-undo.test.ts +++ b/apps/sim/hooks/use-table-undo.test.ts @@ -189,6 +189,7 @@ describe('useTableUndo – delete-column undo cell restore chunking', () => { cellData: [], previousOrder: null, previousWidth: null, + previousPinnedColumns: null, } it('does not call mutateAsync when cellData is empty', async () => { diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index 8a364d54691..289fcc01c70 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -31,6 +31,8 @@ interface UseTableUndoProps { onColumnOrderChange?: (order: string[]) => void onColumnRename?: (oldName: string, newName: string) => void onColumnWidthsChange?: (widths: Record) => void + onPinnedColumnsChange?: (pinned: string[]) => void + getPinnedColumns?: () => string[] getColumnWidths?: () => Record } @@ -40,6 +42,8 @@ export function useTableUndo({ onColumnOrderChange, onColumnRename, onColumnWidthsChange, + onPinnedColumnsChange, + getPinnedColumns, getColumnWidths, }: UseTableUndoProps) { const push = useTableUndoStore((s) => s.push) @@ -69,6 +73,10 @@ export function useTableUndo({ onColumnRenameRef.current = onColumnRename const onColumnWidthsChangeRef = useRef(onColumnWidthsChange) onColumnWidthsChangeRef.current = onColumnWidthsChange + const onPinnedColumnsChangeRef = useRef(onPinnedColumnsChange) + onPinnedColumnsChangeRef.current = onPinnedColumnsChange + const getPinnedColumnsRef = useRef(getPinnedColumns) + getPinnedColumnsRef.current = getPinnedColumns const getColumnWidthsRef = useRef(getColumnWidths) getColumnWidthsRef.current = getColumnWidths @@ -206,11 +214,21 @@ export function useTableUndo({ if (direction === 'undo') { deleteColumnMutation.mutate(action.columnName, { onSuccess: () => { + const metadata: Record = {} const currentWidths = getColumnWidthsRef.current?.() ?? {} if (action.columnName in currentWidths) { const { [action.columnName]: _, ...rest } = currentWidths onColumnWidthsChangeRef.current?.(rest) - updateMetadataMutation.mutate({ columnWidths: rest }) + metadata.columnWidths = rest + } + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (currentPinned.includes(action.columnName)) { + const newPinned = currentPinned.filter((n) => n !== action.columnName) + onPinnedColumnsChangeRef.current?.(newPinned) + metadata.pinnedColumns = newPinned + } + if (Object.keys(metadata).length > 0) { + updateMetadataMutation.mutate(metadata) } }, }) @@ -273,6 +291,27 @@ export function useTableUndo({ metadata.columnWidths = merged onColumnWidthsChangeRef.current?.(merged) } + if (action.previousPinnedColumns !== null) { + const wasColumnPinned = action.previousPinnedColumns.includes( + action.columnName + ) + if (wasColumnPinned) { + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (!currentPinned.includes(action.columnName)) { + const insertIndex = action.previousPinnedColumns.indexOf( + action.columnName + ) + const restoredPinned = [...currentPinned] + restoredPinned.splice( + Math.min(insertIndex, restoredPinned.length), + 0, + action.columnName + ) + onPinnedColumnsChangeRef.current?.(restoredPinned) + metadata.pinnedColumns = restoredPinned + } + } + } if (Object.keys(metadata).length > 0) { updateMetadataMutation.mutate(metadata) } @@ -294,6 +333,14 @@ export function useTableUndo({ metadata.columnWidths = rest onColumnWidthsChangeRef.current?.(rest) } + if (action.previousPinnedColumns !== null) { + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (currentPinned.includes(action.columnName)) { + const newPinned = currentPinned.filter((n) => n !== action.columnName) + onPinnedColumnsChangeRef.current?.(newPinned) + metadata.pinnedColumns = newPinned + } + } if (Object.keys(metadata).length > 0) { updateMetadataMutation.mutate(metadata) } @@ -339,7 +386,21 @@ export function useTableUndo({ } case 'reorder-columns': { - const order = direction === 'undo' ? action.previousOrder : action.newOrder + const restored = direction === 'undo' ? action.previousOrder : action.newOrder + // The user may have pinned/unpinned since the original reorder; + // restoring the raw snapshot can leave a currently-pinned column + // in the middle, which breaks the sticky-offset walk in + // pinnedOffsets and causes the column to jump over its left + // neighbors on scroll. + const pinned = getPinnedColumnsRef.current?.() ?? [] + let order = restored + if (pinned.length > 0) { + const pinnedSet = new Set(pinned) + order = [ + ...restored.filter((n) => pinnedSet.has(n)), + ...restored.filter((n) => !pinnedSet.has(n)), + ] + } onColumnOrderChangeRef.current?.(order) updateMetadataMutation.mutate({ columnOrder: order }) break diff --git a/apps/sim/lib/api/contracts/byok-keys.ts b/apps/sim/lib/api/contracts/byok-keys.ts index d52838eb427..0f79dcb217b 100644 --- a/apps/sim/lib/api/contracts/byok-keys.ts +++ b/apps/sim/lib/api/contracts/byok-keys.ts @@ -20,6 +20,9 @@ export const byokProviderIdSchema = z.enum([ 'cohere', 'hunter', 'peopledatalabs', + 'findymail', + 'prospeo', + 'wiza', ]) export const byokKeySchema = z.object({ diff --git a/apps/sim/lib/api/contracts/logs.ts b/apps/sim/lib/api/contracts/logs.ts index 088d6044a40..fbba88204c9 100644 --- a/apps/sim/lib/api/contracts/logs.ts +++ b/apps/sim/lib/api/contracts/logs.ts @@ -104,6 +104,27 @@ const costSummarySchema = z }) .partial() +/** + * Itemized cost breakdown derived from the usage_log ledger (the single source + * of truth) for the detail view. Each item is one billed line (base fee, a + * model, or a tool/integration); the items reconcile to `total`. + */ +const costLedgerItemSchema = z.object({ + category: z.enum(['fixed', 'model', 'tool']), + description: z.string(), + cost: z.number(), + inputTokens: z.number().optional(), + outputTokens: z.number().optional(), +}) + +export const costLedgerSchema = z.object({ + total: z.number(), + items: z.array(costLedgerItemSchema), +}) + +export type CostLedger = z.output +export type CostLedgerItem = z.output + const pauseSummarySchema = z.object({ status: z.string().nullable(), total: z.number(), @@ -218,7 +239,10 @@ export const workflowLogSummarySchema = z.object({ createdAt: z.string(), workflow: workflowSummarySchema.nullable(), jobTitle: z.string().nullable(), - cost: costSummarySchema.nullable(), + // Top-level run cost is the cost_total projection of the usage_log ledger, + // rendered as { total } (dollars). The itemized breakdown lives in costLedger + // (detail only); per-block costs use the richer costSummarySchema elsewhere. + cost: z.object({ total: z.number() }).nullable(), pauseSummary: pauseSummarySchema, hasPendingPause: z.boolean(), }) @@ -226,6 +250,9 @@ export const workflowLogSummarySchema = z.object({ export const workflowLogDetailSchema = workflowLogSummarySchema.extend({ executionData: executionDataDetailSchema, files: z.array(userFileSchema).nullable(), + // Itemized, ledger-sourced cost breakdown. Null for legacy/pre-ledger runs, + // where the UI falls back to the (reconciling) cost jsonb. + costLedger: costLedgerSchema.nullable().optional(), }) export type WorkflowLogSummary = z.output @@ -236,7 +263,7 @@ export type WorkflowLogDetail = z.output * UI surfaces that render the same log before and after its detail query resolves. */ export type WorkflowLogRow = WorkflowLogSummary & - Partial> + Partial> export const listLogsResponseSchema = z.object({ data: z.array(workflowLogSummarySchema), diff --git a/apps/sim/lib/api/contracts/permission-groups.ts b/apps/sim/lib/api/contracts/permission-groups.ts index 3b0bd1652e4..09bfe87176d 100644 --- a/apps/sim/lib/api/contracts/permission-groups.ts +++ b/apps/sim/lib/api/contracts/permission-groups.ts @@ -5,6 +5,7 @@ import { permissionGroupConfigSchema } from '@/lib/permission-groups/types' export const permissionGroupFullConfigSchema = z.object({ allowedIntegrations: z.array(z.string()).nullable(), allowedModelProviders: z.array(z.string()).nullable(), + deniedModels: z.array(z.string()).default([]), hideTraceSpans: z.boolean(), hideKnowledgeBaseTab: z.boolean(), hideTablesTab: z.boolean(), diff --git a/apps/sim/lib/api/contracts/subscription.ts b/apps/sim/lib/api/contracts/subscription.ts index c8b486deaf5..ee1498ddac1 100644 --- a/apps/sim/lib/api/contracts/subscription.ts +++ b/apps/sim/lib/api/contracts/subscription.ts @@ -103,7 +103,6 @@ export const organizationBillingMemberSchema = z userName: z.string().nullable().optional(), userEmail: z.string().nullable().optional(), joinedAt: z.string().nullable().optional(), - lastActive: z.string().nullable().optional(), }) .passthrough() diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index aadb38f1352..f56c22a1222 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -135,6 +135,7 @@ export const deleteTableColumnBodySchema = z.object({ export const tableMetadataSchema = z.object({ columnWidths: z.record(z.string(), z.number().positive()).optional(), columnOrder: z.array(z.string()).optional(), + pinnedColumns: z.array(z.string()).optional(), }) satisfies z.ZodType export const updateTableMetadataBodySchema = z.object({ diff --git a/apps/sim/lib/api/contracts/v1/admin/organizations.ts b/apps/sim/lib/api/contracts/v1/admin/organizations.ts index 9dad65f197f..1281b5e649d 100644 --- a/apps/sim/lib/api/contracts/v1/admin/organizations.ts +++ b/apps/sim/lib/api/contracts/v1/admin/organizations.ts @@ -43,7 +43,6 @@ export const adminV1MemberSchema = z.object({ export const adminV1MemberDetailSchema = adminV1MemberSchema.extend({ currentPeriodCost: z.string(), currentUsageLimit: z.string().nullable(), - lastActive: z.string().nullable(), billingBlocked: z.boolean(), }) @@ -75,18 +74,6 @@ export const adminV1SeatAnalyticsSchema = z.object({ subscriptionPlan: z.string(), canAddSeats: z.boolean(), utilizationRate: z.number(), - activeMembers: z.number(), - inactiveMembers: z.number(), - memberActivity: z.array( - z.object({ - userId: z.string(), - userName: z.string(), - userEmail: z.string(), - role: z.string(), - joinedAt: z.string(), - lastActive: z.string().nullable(), - }) - ), }) export const adminV1CreateOrganizationBodySchema = z.object({ diff --git a/apps/sim/lib/api/contracts/v1/admin/users.ts b/apps/sim/lib/api/contracts/v1/admin/users.ts index 6610134d93f..34bc00f5e22 100644 --- a/apps/sim/lib/api/contracts/v1/admin/users.ts +++ b/apps/sim/lib/api/contracts/v1/admin/users.ts @@ -23,27 +23,14 @@ export const adminV1UserBillingSchema = z.object({ userName: z.string(), userEmail: z.string(), stripeCustomerId: z.string().nullable(), - totalManualExecutions: z.number(), - totalApiCalls: z.number(), - totalWebhookTriggers: z.number(), - totalScheduledExecutions: z.number(), - totalChatExecutions: z.number(), - totalMcpExecutions: z.number(), - totalA2aExecutions: z.number(), - totalTokensUsed: z.number(), - totalCost: z.string(), currentUsageLimit: z.string().nullable(), currentPeriodCost: z.string(), lastPeriodCost: z.string().nullable(), billedOverageThisPeriod: z.string(), storageUsedBytes: z.number(), - lastActive: z.string().nullable(), billingBlocked: z.boolean(), - totalCopilotCost: z.string(), currentPeriodCopilotCost: z.string(), lastPeriodCopilotCost: z.string().nullable(), - totalCopilotTokens: z.number(), - totalCopilotCalls: z.number(), }) export const adminV1UserBillingWithSubscriptionSchema = adminV1UserBillingSchema.extend({ diff --git a/apps/sim/lib/api/contracts/v1/logs.ts b/apps/sim/lib/api/contracts/v1/logs.ts index 6659ccb99ad..52e48b096d2 100644 --- a/apps/sim/lib/api/contracts/v1/logs.ts +++ b/apps/sim/lib/api/contracts/v1/logs.ts @@ -27,7 +27,14 @@ export const v1ListLogsQuerySchema = z.object({ details: z.enum(['basic', 'full']).optional().default('basic'), includeTraceSpans: booleanQueryFlagSchema.optional().default(false), includeFinalOutput: booleanQueryFlagSchema.optional().default(false), - limit: z.coerce.number().optional().default(100), + // Clamp rather than reject: this limit was previously unbounded, so a hard + // .max() would 400 existing consumers passing a larger value. Page size is + // still capped to bound response size and materialization reads. + limit: z.coerce + .number() + .optional() + .default(100) + .transform((v) => Math.min(Math.max(1, Math.trunc(v)), 1000)), cursor: z.string().optional(), order: z.enum(['desc', 'asc']).optional().default('desc'), }) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index faffc248668..e84123de557 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -78,6 +78,7 @@ import { isOrganizationsEnabled, isRegistrationDisabled, isSignupEmailValidationEnabled, + isSignupMxValidationEnabled, } from '@/lib/core/config/feature-flags' import { PlatformEvents } from '@/lib/core/telemetry' import { getBaseUrl, isLocalhostUrl, parseOriginList } from '@/lib/core/utils/urls' @@ -85,6 +86,7 @@ import { processCredentialDraft } from '@/lib/credentials/draft-processor' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils' import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { validateSignupEmailMx } from '@/lib/messaging/email/validation.server' import { scheduleLifecycleEmail } from '@/lib/messaging/lifecycle' import { captureServerEvent, getPostHogClient } from '@/lib/posthog/server' import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' @@ -843,6 +845,15 @@ export const auth = betterAuth({ }) } + if (isSignupMxValidationEnabled && ctx.path.startsWith('/sign-up/email') && ctx.body?.email) { + const mxCheck = await validateSignupEmailMx(ctx.body.email) + if (!mxCheck.allowed) { + throw new APIError('FORBIDDEN', { + message: 'Sign-ups from this email domain are not allowed.', + }) + } + } + if (ctx.path === '/oauth2/authorize' || ctx.path === '/oauth2/token') { const clientId = (ctx.query?.client_id ?? ctx.body?.client_id) as string | undefined if (clientId && isMetadataUrl(clientId)) { diff --git a/apps/sim/lib/billing/client/upgrade.ts b/apps/sim/lib/billing/client/upgrade.ts index bbd0b5f8fe7..eae23a285d5 100644 --- a/apps/sim/lib/billing/client/upgrade.ts +++ b/apps/sim/lib/billing/client/upgrade.ts @@ -41,7 +41,8 @@ export function useSubscriptionUpgrade() { throw new Error('User not authenticated') } - let currentSubscriptionId: string | undefined + let currentSubscriptionRowId: string | undefined + let currentStripeSubscriptionId: string | undefined let allSubscriptions: any[] = [] try { const listResult = await client.subscription.list() @@ -49,9 +50,22 @@ export function useSubscriptionUpgrade() { const activePersonalSub = allSubscriptions.find( (sub: any) => hasPaidSubscriptionStatus(sub.status) && sub.referenceId === userId ) - currentSubscriptionId = activePersonalSub?.id + currentSubscriptionRowId = activePersonalSub?.id + currentStripeSubscriptionId = activePersonalSub?.stripeSubscriptionId } catch (_e) { - currentSubscriptionId = undefined + currentSubscriptionRowId = undefined + currentStripeSubscriptionId = undefined + } + + if (currentSubscriptionRowId && !currentStripeSubscriptionId) { + logger.error('Active paid subscription is missing its Stripe subscription ID', { + userId, + subscriptionRowId: currentSubscriptionRowId, + targetPlan, + }) + throw new Error( + 'We could not match your current plan with our payment provider. Please contact support before upgrading so you are not charged twice.' + ) } let referenceId = userId @@ -137,36 +151,45 @@ export function useSubscriptionUpgrade() { ...(annual && { annual: true }), } as const - const finalParams = currentSubscriptionId - ? { ...upgradeParams, subscriptionId: currentSubscriptionId } + const finalParams = currentStripeSubscriptionId + ? { ...upgradeParams, subscriptionId: currentStripeSubscriptionId } : upgradeParams logger.info( - currentSubscriptionId ? 'Upgrading existing subscription' : 'Creating new subscription', - { targetPlan, planName, annual, currentSubscriptionId, referenceId } + currentStripeSubscriptionId + ? 'Upgrading existing subscription' + : 'Creating new subscription', + { + targetPlan, + planName, + annual, + currentStripeSubscriptionId, + currentSubscriptionRowId, + referenceId, + } ) await betterAuthSubscription.upgrade(finalParams) - if (targetPlan === 'team' && currentSubscriptionId && referenceId !== userId) { + if (targetPlan === 'team' && currentSubscriptionRowId && referenceId !== userId) { try { logger.info('Transferring subscription to organization after upgrade', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, }) try { await requestJson(subscriptionTransferContract, { - params: { id: currentSubscriptionId }, + params: { id: currentSubscriptionRowId }, body: { organizationId: referenceId }, }) logger.info('Successfully transferred subscription to organization', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, }) } catch (transferError) { logger.error('Failed to transfer subscription to organization', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, error: transferError instanceof ApiClientError diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 8595277c385..2db6b3ef7c7 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -7,7 +7,7 @@ import { type SubscriptionMetadata, } from '@/lib/billing/core/subscription' import { getOrgUsageLimit, getUserUsageData } from '@/lib/billing/core/usage' -import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { COPILOT_USAGE_SOURCES, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' import { getCreditBalance } from '@/lib/billing/credits/balance' import { computeDailyRefreshConsumed, @@ -128,6 +128,8 @@ async function aggregateOrgMemberStats(organizationId: string): Promise<{ .where(eq(member.organizationId, organizationId)) let currentPeriodCost = new Decimal(0) + // Copilot baseline (copilot source). All copilot-family usage (incl. MCP) lives + // in usage_log and is added via the copilot ledger by callers — not a baseline. let currentPeriodCopilotCost = new Decimal(0) let lastPeriodCopilotCost = new Decimal(0) const memberIds: string[] = [] @@ -454,20 +456,34 @@ export async function getSimplifiedBillingSummary( const pooled = await aggregateOrgMemberStats(organizationId) const rawCurrentUsage = pooled.currentPeriodCost - const totalCopilotCost = pooled.currentPeriodCopilotCost const totalLastPeriodCopilotCost = pooled.lastPeriodCopilotCost // Deduct daily-refresh credits against this specific org's pool. // `usageData` is derived from the caller's priority subscription // and may not match the requested org (multi-org admins, personal // priority sub, etc.), so it cannot be reused here. - const ledgerUsage = + const orgBillingPeriod = subscription.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + const ledgerUsage = orgBillingPeriod + ? await getBillingPeriodUsageCost( + { type: 'organization', id: organizationId }, + orgBillingPeriod + ) + : 0 + // Copilot breakdown = member baselines (copilot + MCP) + the copilot-family + // ledger for the period (COPILOT_USAGE_SOURCES: copilot/workspace-chat/ + // mcp_copilot/mothership_block); the baseline columns are no longer incremented. + const totalCopilotCost = + pooled.currentPeriodCopilotCost + + (orgBillingPeriod ? await getBillingPeriodUsageCost( { type: 'organization', id: organizationId }, - { start: subscription.periodStart, end: subscription.periodEnd } + orgBillingPeriod, + COPILOT_USAGE_SOURCES ) - : 0 + : 0) let refreshDeduction = 0 if (isPaid(plan) && subscription.periodStart) { const planDollars = getPlanTierDollars(plan) @@ -561,6 +577,8 @@ export async function getSimplifiedBillingSummary( .where(eq(userStats.userId, userId)) .limit(1) + // Copilot baseline (copilot source). MCP copilot usage lives in usage_log and + // is added via the copilot ledger below, not a userStats baseline. const copilotCost = userStatsRows.length > 0 ? toNumber(toDecimal(userStatsRows[0].currentPeriodCopilotCost)) : 0 @@ -576,6 +594,25 @@ export async function getSimplifiedBillingSummary( totalLastPeriodCopilotCost = pooled.lastPeriodCopilotCost } + // Add the copilot-family ledger (COPILOT_USAGE_SOURCES: copilot/workspace-chat/ + // mcp_copilot/mothership_block) on top of the baseline; those columns are no + // longer incremented per usage. + const copilotBillingPeriod = + usageData.billingPeriodStart && usageData.billingPeriodEnd + ? { start: usageData.billingPeriodStart, end: usageData.billingPeriodEnd } + : null + if (copilotBillingPeriod) { + const copilotEntity = + orgScoped && subscription?.referenceId + ? ({ type: 'organization', id: subscription.referenceId } as const) + : ({ type: 'user', id: userId } as const) + totalCopilotCost += await getBillingPeriodUsageCost( + copilotEntity, + copilotBillingPeriod, + COPILOT_USAGE_SOURCES + ) + } + const percentUsed = usageData.limit > 0 ? (currentUsage / usageData.limit) * 100 : 0 const daysRemaining = usageData.billingPeriodEnd diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index eed717003a3..dd3389213f9 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -4,7 +4,10 @@ import { createLogger } from '@sim/logger' import { and, count, eq, gt, ne } from 'drizzle-orm' import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { getOrganizationSubscription, getPlanPricing } from '@/lib/billing/core/billing' -import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { + getBillingPeriodUsageCost, + getBillingPeriodUsageCostByUser, +} from '@/lib/billing/core/usage-log' import { computeDailyRefreshConsumed, getOrgMemberRefreshBounds, @@ -50,7 +53,32 @@ interface MemberUsageData { isOverLimit: boolean role: string joinedAt: Date - lastActive: Date | null +} + +/** + * Per-member usage_log cost for an org's current billing period, keyed by userId. + * `currentPeriodCost` is only a baseline (no longer incremented on the hot path), + * so callers add this ledger component to it for each member's real current-period + * usage. Pass `period` to reuse an already-fetched subscription window; omit it to + * look up the org's subscription here. Returns an empty map when there's no period. + */ +export async function getOrgMemberLedgerByUser( + organizationId: string, + period?: { start: Date; end: Date } | null +): Promise> { + let billingPeriod = period ?? null + if (period === undefined) { + const subscription = await getOrganizationSubscription(organizationId) + billingPeriod = + subscription?.periodStart && subscription?.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + } + if (!billingPeriod) return new Map() + return getBillingPeriodUsageCostByUser( + { type: 'organization', id: organizationId }, + billingPeriod + ) } /** @@ -93,16 +121,25 @@ export async function getOrganizationBillingData( // User stats fields currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, }) .from(member) .innerJoin(user, eq(member.userId, user.id)) .leftJoin(userStats, eq(member.userId, userStats.userId)) .where(eq(member.organizationId, organizationId)) + // Per-member current-period usage = userStats baseline + attributed usage_log + // rows. currentPeriodCost is no longer incremented on the hot path, so the + // baseline alone under-reports; add each member's ledger sum for the period. + const billingPeriod = + subscription.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + const usageByUser = await getOrgMemberLedgerByUser(organizationId, billingPeriod) + // Process member data const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => { - const currentUsage = Number(memberRecord.currentPeriodCost || 0) + const currentUsage = + Number(memberRecord.currentPeriodCost || 0) + (usageByUser.get(memberRecord.userId) ?? 0) const usageLimit = Number(memberRecord.currentUsageLimit || getFreeTierLimit()) const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0 @@ -116,17 +153,22 @@ export async function getOrganizationBillingData( isOverLimit: currentUsage > usageLimit, role: memberRecord.role, joinedAt: memberRecord.joinedAt, - lastActive: memberRecord.lastActive, } }) - // Calculate aggregated statistics - let totalCurrentUsage = members.reduce((sum, m) => sum + m.currentUsage, 0) - - if (subscription.periodStart && subscription.periodEnd) { + // Authoritative org total = member baselines + the org's full usage_log for + // the period (also captures rows from members no longer present). Computed + // from raw baselines, NOT members[].currentUsage — the latter already folds + // in per-member usage_log for display, so summing it AND adding the org + // ledger would double-count. + let totalCurrentUsage = membersWithUsage.reduce( + (sum, m) => sum + Number(m.currentPeriodCost || 0), + 0 + ) + if (billingPeriod) { totalCurrentUsage += await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, - { start: subscription.periodStart, end: subscription.periodEnd } + billingPeriod ) } diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 6232a1baf3a..ad1657a6db6 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -4,18 +4,18 @@ import { usageLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, desc, eq, gte, lte, sql } from 'drizzle-orm' +import { and, desc, eq, gte, inArray, lte, sql } from 'drizzle-orm' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import type { DbOrTx } from '@/lib/db/types' const logger = createLogger('UsageLog') /** * Usage log category types */ -export type UsageLogCategory = 'model' | 'fixed' +export type UsageLogCategory = 'model' | 'fixed' | 'tool' /** * Usage log source types @@ -31,6 +31,18 @@ export type UsageLogSource = | 'voice-input' | 'enrichment' +/** + * usage_log sources that make up the "copilot" cost breakdown shown in billing + * summaries: the copilot agent, mothership/workspace chat, MCP copilot, and + * mothership blocks. Mirrors the source set billed via /api/billing/update-cost. + */ +export const COPILOT_USAGE_SOURCES: UsageLogSource[] = [ + 'copilot', + 'workspace-chat', + 'mcp_copilot', + 'mothership_block', +] + /** * Metadata for 'model' category charges */ @@ -47,7 +59,7 @@ export type UsageLogMetadata = ModelUsageMetadata | Record | nu export type BillingEntityType = 'user' | 'organization' -interface BillingEntity { +export interface BillingEntity { type: BillingEntityType id: string } @@ -84,9 +96,16 @@ export interface RecordUsageParams { billingEntity?: BillingEntity /** Billing period bounds, resolved by caller when already known. */ billingPeriod?: { start: Date; end: Date } + /** + * Optional transaction to run the ledger INSERT in. Callers that reconcile a + * read-then-insert under a lock (e.g. the per-execution advisory lock in the + * workflow completion path) pass their tx so the insert participates in the + * same locked transaction. Defaults to the pooled db. + */ + tx?: DbOrTx } -function stableEventKey(parts: Record): string { +export function stableEventKey(parts: Record): string { const payload = Object.keys(parts) .sort() .map((key) => `${key}:${String(parts[key] ?? '')}`) @@ -94,32 +113,53 @@ function stableEventKey(parts: Record): string { return createHash('sha256').update(payload).digest('hex') } +type ResolvedSubscription = Awaited> + +export interface BillingContext { + billingEntity: BillingEntity + billingPeriod: { start: Date; end: Date } +} + +/** + * Derive the billing entity + period from an ALREADY-resolved subscription. + * Callers that already hold the subscription (e.g. the workflow completion path, + * which fetches it for usage-threshold emails) can derive the context once and + * pass it into recordUsage so resolveBillingContext skips a redundant lookup. + * This is the single source of the entity/period derivation — keep it the only + * place that maps a subscription to a billing context. + */ +export function deriveBillingContext( + userId: string, + subscription: ResolvedSubscription +): BillingContext { + const billingEntity: BillingEntity = + subscription && isOrgScopedSubscription(subscription, userId) + ? { type: 'organization', id: subscription.referenceId } + : { type: 'user', id: userId } + + const billingPeriod = + subscription?.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : defaultBillingPeriod() + + return { billingEntity, billingPeriod } +} + async function resolveBillingContext( userId: string, billingEntity?: BillingEntity, billingPeriod?: { start: Date; end: Date } -): Promise<{ - billingEntity: BillingEntity - billingPeriod: { start: Date; end: Date } -}> { +): Promise { if (billingEntity && billingPeriod) { return { billingEntity, billingPeriod } } const subscription = await getHighestPrioritySubscription(userId) - const resolvedEntity = - billingEntity ?? - (subscription && isOrgScopedSubscription(subscription, userId) - ? { type: 'organization' as const, id: subscription.referenceId } - : { type: 'user' as const, id: userId }) - - const resolvedPeriod = - billingPeriod ?? - (subscription?.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod()) - - return { billingEntity: resolvedEntity, billingPeriod: resolvedPeriod } + const derived = deriveBillingContext(userId, subscription) + return { + billingEntity: billingEntity ?? derived.billingEntity, + billingPeriod: billingPeriod ?? derived.billingPeriod, + } } /** @@ -128,43 +168,55 @@ async function resolveBillingContext( */ export async function getBillingPeriodUsageCost( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date } + billingPeriod: { start: Date; end: Date }, + source?: UsageLogSource | UsageLogSource[] ): Promise { + const conditions = [ + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ] + if (source) { + conditions.push( + Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) + ) + } + const [row] = await db .select({ cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, }) .from(usageLog) - .where( - and( - eq(usageLog.billingEntityType, billingEntity.type), - eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) - ) - ) + .where(and(...conditions)) return Number.parseFloat(row?.cost ?? '0') } export async function getBillingPeriodUsageCostByUser( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date } + billingPeriod: { start: Date; end: Date }, + source?: UsageLogSource | UsageLogSource[] ): Promise> { + const conditions = [ + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ] + if (source) { + conditions.push( + Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) + ) + } + const rows = await db .select({ userId: usageLog.userId, cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, }) .from(usageLog) - .where( - and( - eq(usageLog.billingEntityType, billingEntity.type), - eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) - ) - ) + .where(and(...conditions)) .groupBy(usageLog.userId) return new Map(rows.map((row) => [row.userId, Number.parseFloat(row.cost ?? '0')])) @@ -178,12 +230,20 @@ export async function getBillingPeriodUsageCostByUser( * but usage writes no longer contend on the user_stats row. */ export async function recordUsage(params: RecordUsageParams): Promise { - if (!isBillingEnabled) { - return - } - - const { userId, entries, workspaceId, workflowId, executionId, billingEntity, billingPeriod } = - params + // The usage ledger is written regardless of BILLING_ENABLED so it is the + // single, universal source of truth for cost (including self-hosted, where + // it powers the logs-page cost display). Billing *enforcement* (Stripe / + // overage) is gated separately by callers, not here. + const { + userId, + entries, + workspaceId, + workflowId, + executionId, + billingEntity, + billingPeriod, + tx, + } = params const validEntries = entries.filter((e) => e.cost > 0) @@ -193,7 +253,7 @@ export async function recordUsage(params: RecordUsageParams): Promise { const context = await resolveBillingContext(userId, billingEntity, billingPeriod) - const insertedRows = await db + const insertedRows = await (tx ?? db) .insert(usageLog) .values( validEntries.map((entry, index) => { diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index 06eba35faae..972a199b0fb 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -607,51 +607,6 @@ export async function syncUsageLimitsFromSubscription(userId: string): Promise -> { - try { - const teamMembers = await db - .select({ - userId: member.userId, - userName: user.name, - userEmail: user.email, - currentLimit: userStats.currentUsageLimit, - currentPeriodCost: userStats.currentPeriodCost, - totalCost: userStats.totalCost, - lastActive: userStats.lastActive, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - - return teamMembers.map((memberData) => ({ - userId: memberData.userId, - userName: memberData.userName, - userEmail: memberData.userEmail, - currentLimit: toNumber(toDecimal(memberData.currentLimit || getFreeTierLimit().toString())), - currentUsage: toNumber(toDecimal(memberData.currentPeriodCost)), - totalCost: toNumber(toDecimal(memberData.totalCost)), - lastActive: memberData.lastActive, - })) - } catch (error) { - logger.error('Failed to get team usage limits', { organizationId, error }) - return [] - } -} - /** * Returns the effective current period usage cost for a user, with daily * refresh credits deducted. Org-scoped subs return the pooled sum across diff --git a/apps/sim/lib/billing/credits/conversion.test.ts b/apps/sim/lib/billing/credits/conversion.test.ts new file mode 100644 index 00000000000..7d4a45a3733 --- /dev/null +++ b/apps/sim/lib/billing/credits/conversion.test.ts @@ -0,0 +1,65 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + apportionCredits, + dollarsToCredits, + formatCreditCost, +} from '@/lib/billing/credits/conversion' + +describe('formatCreditCost', () => { + it('renders multiplier-inclusive dollars as a single-rounded credit label', () => { + expect(formatCreditCost(0.005)).toBe('1 credit') + expect(formatCreditCost(0.03141848)).toBe('6 credits') + expect(formatCreditCost(1.234)).toBe('247 credits') + }) + + it('distinguishes sub-credit charges from zero', () => { + expect(formatCreditCost(0.001)).toBe('<1 credit') + expect(formatCreditCost(0)).toBe('0 credits') + }) + + it('honors emptyForZeroOrLess for the trace view contract', () => { + expect(formatCreditCost(0, { emptyForZeroOrLess: true })).toBeUndefined() + expect(formatCreditCost(undefined, { emptyForZeroOrLess: true })).toBeUndefined() + expect(formatCreditCost(undefined)).toBe('—') + }) +}) + +describe('apportionCredits', () => { + it('keeps line items summing exactly to the rounded total (no round-then-sum drift)', () => { + // Real execution 43ef064d: base + 2x model, multiplier already applied. + // Round-then-sum would give 1 + 4 + 2 = 7; the true total is 6. + const credits = apportionCredits([ + { key: 'base', dollars: 0.005 }, + { key: 'input', dollars: 0.018798 }, + { key: 'output', dollars: 0.00762 }, + { key: 'tool', dollars: 0 }, + ]) + + const total = dollarsToCredits(0.005 + 0.018798 + 0.00762 + 0) + expect(total).toBe(6) + expect(credits.base + credits.input + credits.output + credits.tool).toBe(total) + expect(credits.base).toBe(1) + }) + + it('handles all-zero components', () => { + const credits = apportionCredits([ + { key: 'base', dollars: 0 }, + { key: 'model', dollars: 0 }, + ]) + expect(credits.base + credits.model).toBe(0) + }) + + it('ignores negative/non-finite components without throwing', () => { + const credits = apportionCredits([ + { key: 'base', dollars: 0.005 }, + { key: 'model', dollars: Number.NaN }, + { key: 'tool', dollars: -1 }, + ]) + expect(credits.base).toBe(1) + expect(credits.model).toBe(0) + expect(credits.tool).toBe(0) + }) +}) diff --git a/apps/sim/lib/billing/credits/conversion.ts b/apps/sim/lib/billing/credits/conversion.ts index a4f241f9954..74a77b8fdaa 100644 --- a/apps/sim/lib/billing/credits/conversion.ts +++ b/apps/sim/lib/billing/credits/conversion.ts @@ -12,6 +12,83 @@ export function dollarsToCredits(dollars: number): number { return Math.round(dollars * CREDIT_MULTIPLIER) } +/** + * Single source of truth for rendering a dollar cost as a credit label. + * + * Both the billing cost breakdown and the trace view derive their credit + * strings from here so the two surfaces can never diverge in rounding, + * thresholds, or pluralization. The dollar amount passed in is expected to + * already carry any cost multiplier (the value is converted with one — and + * only one — round via `dollarsToCredits`, i.e. multiply-then-round; never + * round per-line then multiply). + * + * `emptyForZeroOrLess` controls the zero/empty behavior so existing call sites + * keep their contracts: the breakdown wants a concrete "0 credits"/"—" string, + * the trace view wants `undefined` so it can hide the chip entirely. + */ +export function formatCreditCost( + dollars: number | null | undefined, + opts?: { emptyForZeroOrLess?: boolean } +): string | undefined { + if (dollars === undefined || dollars === null || !Number.isFinite(dollars)) { + return opts?.emptyForZeroOrLess ? undefined : '—' + } + + const credits = dollarsToCredits(dollars) + + if (credits <= 0) { + if (dollars > 0) return '<1 credit' + return opts?.emptyForZeroOrLess ? undefined : '0 credits' + } + + return `${credits.toLocaleString()} ${credits === 1 ? 'credit' : 'credits'}` +} + +/** + * Splits a set of cost components into integer credits that sum *exactly* to + * the credits of their combined total. + * + * This is the fix for the "line items don't add up to the total" class of bug: + * rounding each line independently (round-then-sum) drifts from the real charge + * (e.g. 1 + 2 + 1 = 4, or 1 + 4 + 2 = 7, when the true total is 6). Instead we + * convert the *summed* dollars to credits with a single round (multiply-then- + * round) and distribute that figure across the components via the largest- + * remainder method, so every component is multiplier-applied, internally + * consistent, and the rows always reconcile with the total. + * + * Each component's `dollars` is expected to already include any cost multiplier. + */ +export function apportionCredits( + components: { key: K; dollars: number }[] +): Record { + const result = {} as Record + + const sanitized = components.map((c) => ({ + key: c.key, + dollars: Number.isFinite(c.dollars) && c.dollars > 0 ? c.dollars : 0, + })) + + const totalDollars = sanitized.reduce((sum, c) => sum + c.dollars, 0) + const targetCredits = dollarsToCredits(totalDollars) + + const exact = sanitized.map((c) => ({ + key: c.key, + floor: Math.floor(c.dollars * CREDIT_MULTIPLIER), + frac: c.dollars * CREDIT_MULTIPLIER - Math.floor(c.dollars * CREDIT_MULTIPLIER), + })) + + for (const c of exact) result[c.key] = c.floor + + let remainder = targetCredits - exact.reduce((sum, c) => sum + c.floor, 0) + const byFraction = [...exact].sort((a, b) => b.frac - a.frac) + for (let i = 0; i < byFraction.length && remainder > 0; i++) { + result[byFraction[i].key] += 1 + remainder-- + } + + return result +} + /** * Format a dollar amount as a comma-separated credit string. * Values at or above the on-demand unlimited threshold display as ∞. diff --git a/apps/sim/lib/billing/index.ts b/apps/sim/lib/billing/index.ts index 8c01b7928c6..0df31e11d88 100644 --- a/apps/sim/lib/billing/index.ts +++ b/apps/sim/lib/billing/index.ts @@ -25,7 +25,6 @@ export { export * from '@/lib/billing/core/usage' export { checkUsageStatus, - getTeamUsageLimits, getUserUsageData as getUsageData, getUserUsageLimit as getUsageLimit, updateUserUsageLimit as updateUsageLimit, diff --git a/apps/sim/lib/billing/types/index.ts b/apps/sim/lib/billing/types/index.ts index db8b5adde3b..c90d0991c98 100644 --- a/apps/sim/lib/billing/types/index.ts +++ b/apps/sim/lib/billing/types/index.ts @@ -137,8 +137,6 @@ interface TeamUsageLimit { userEmail: string currentLimit: number currentUsage: number - totalCost: number - lastActive: Date | null limitSetBy: string | null limitUpdatedAt: Date | null } diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index df9a83d8e72..230ebee46aa 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { invitation, member, organization, subscription, user, userStats } from '@sim/db/schema' +import { invitation, member, organization, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, eq, gt, ne } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' @@ -307,35 +307,15 @@ export async function getOrganizationSeatAnalytics(organizationId: string) { return null } - const memberActivity = await db - .select({ - userId: member.userId, - userName: user.name, - userEmail: user.email, - role: member.role, - joinedAt: member.createdAt, - lastActive: userStats.lastActive, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - const utilizationRate = seatInfo.maxSeats > 0 ? (seatInfo.currentSeats / seatInfo.maxSeats) * 100 : 0 - const recentlyActive = memberActivity.filter((memberData) => { - if (!memberData.lastActive) return false - const daysSinceActive = (Date.now() - memberData.lastActive.getTime()) / (1000 * 60 * 60 * 24) - return daysSinceActive <= 30 // Active in last 30 days - }).length - + // Member activity analytics (active/inactive counts, memberActivity) were + // derived from userStats.lastActive, which is no longer written. Dropped + // rather than report frozen data; reintroduce with a real activity source. return { ...seatInfo, utilizationRate: Math.round(utilizationRate * 100) / 100, - activeMembers: recentlyActive, - inactiveMembers: seatInfo.currentSeats - recentlyActive, - memberActivity, } } catch (error) { logger.error('Failed to get organization seat analytics', { organizationId, error }) diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index 0a30d364f36..9460f521623 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -13,7 +13,10 @@ import type Stripe from 'stripe' import { getEmailSubject, PaymentFailedEmail, renderCreditPurchaseEmail } from '@/components/emails' import { BILLING_LOCK_TIMEOUT_MS } from '@/lib/billing/constants' import { calculateSubscriptionOverage, isSubscriptionOrgScoped } from '@/lib/billing/core/billing' -import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log' +import { + COPILOT_USAGE_SOURCES, + getBillingPeriodUsageCostByUser, +} from '@/lib/billing/core/usage-log' import { addCredits, getCreditBalanceForEntity } from '@/lib/billing/credits/balance' import { setUsageLimitForCredits } from '@/lib/billing/credits/purchase' import { blockOrgMembers, unblockOrgMembers } from '@/lib/billing/organizations/membership' @@ -424,6 +427,15 @@ export async function resetUsageForSubscription(sub: { billingPeriod ) : new Map() + // Copilot-family ledger per user, so last-period copilot mirrors last-period + // cost (baseline + usage_log) instead of capturing the baseline alone. + const copilotLedgerByUser = billingPeriod + ? await getBillingPeriodUsageCostByUser( + { type: 'organization', id: sub.referenceId }, + billingPeriod, + COPILOT_USAGE_SOURCES + ) + : new Map() await db.transaction(async (tx) => { await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) @@ -494,15 +506,27 @@ export async function resetUsageForSubscription(sub: { memberStatsRows.map((row) => sql`WHEN ${row.userId} THEN ${row.currentCopilot ?? '0'}`), sql` ` ) + // Last-period copilot = baseline copilot + copilot-family ledger, mirroring + // lastPeriodCost. (The reset below still subtracts only the baseline, since + // the ledger is period-scoped and rolls over on its own.) + const lastCopilotCostByUser = sql.join( + memberStatsRows.map((row) => { + const baselineCopilot = toNumber(toDecimal(row.currentCopilot)) + const copilotLedger = copilotLedgerByUser.get(row.userId) ?? 0 + return sql`WHEN ${row.userId} THEN ${(baselineCopilot + copilotLedger).toString()}` + }), + sql` ` + ) const capturedLastCost = sql`CASE ${userStats.userId} ${lastCostByUser} ELSE '0' END` const capturedCurrentCost = sql`CASE ${userStats.userId} ${currentCostByUser} ELSE '0' END` const capturedCurrentCopilotCost = sql`CASE ${userStats.userId} ${currentCopilotCostByUser} ELSE '0' END` + const capturedLastCopilotCost = sql`CASE ${userStats.userId} ${lastCopilotCostByUser} ELSE '0' END` await tx .update(userStats) .set({ lastPeriodCost: capturedLastCost, - lastPeriodCopilotCost: capturedCurrentCopilotCost, + lastPeriodCopilotCost: capturedLastCopilotCost, currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - (${capturedCurrentCost})::decimal)`, currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - (${capturedCurrentCopilotCost})::decimal)`, billedOverageThisPeriod: '0', @@ -536,6 +560,15 @@ export async function resetUsageForSubscription(sub: { ) : new Map() const userLedgerUsage = ledgerUsage.get(sub.referenceId) ?? 0 + const copilotLedgerUsage = billingPeriod + ? (( + await getBillingPeriodUsageCostByUser( + { type: 'user', id: sub.referenceId }, + billingPeriod, + COPILOT_USAGE_SOURCES + ) + ).get(sub.referenceId) ?? 0) + : 0 // Snapshot > 0: user joined a paid org mid-cycle. The pre-join // portion was billed on this invoice (snapshot); `currentPeriodCost` @@ -546,7 +579,12 @@ export async function resetUsageForSubscription(sub: { .update(userStats) .set({ lastPeriodCost: (snapshot + userLedgerUsage).toString(), - lastPeriodCopilotCost: '0', + // Pre-join personal copilot = the user-scoped copilot ledger only + // (post-join copilot usage is org-attributed, so this captures the + // pre-join portion). The copilot baseline stays with the org via the + // retained currentPeriodCopilotCost, so don't add it here (avoids a + // double count at the org's cycle-close). + lastPeriodCopilotCost: copilotLedgerUsage.toString(), proPeriodCostSnapshot: '0', proPeriodCostSnapshotAt: null, billedOverageThisPeriod: '0', @@ -563,7 +601,9 @@ export async function resetUsageForSubscription(sub: { .update(userStats) .set({ lastPeriodCost: totalLastPeriod, - lastPeriodCopilotCost: currentCopilot, + lastPeriodCopilotCost: ( + toNumber(toDecimal(currentCopilot)) + copilotLedgerUsage + ).toString(), currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - ${current}::decimal)`, currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - ${currentCopilot}::decimal)`, proPeriodCostSnapshot: '0', diff --git a/apps/sim/lib/copilot/chat/messages-dual-write.test.ts b/apps/sim/lib/copilot/chat/messages-dual-write.test.ts index f2d631e8dac..17d3e1666c5 100644 --- a/apps/sim/lib/copilot/chat/messages-dual-write.test.ts +++ b/apps/sim/lib/copilot/chat/messages-dual-write.test.ts @@ -26,6 +26,12 @@ const assistantMsg: PersistedMessage = { timestamp: '2026-01-01T00:00:01.000Z', } +/** The first arg passed to the most recent `.values(...)` call. */ +function lastValuesRows() { + const calls = dbChainMockFns.values.mock.calls + return calls[calls.length - 1][0] as Array> +} + describe('messages-dual-write', () => { beforeEach(() => { vi.clearAllMocks() @@ -43,7 +49,7 @@ describe('messages-dual-write', () => { expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1) expect(dbChainMockFns.values).toHaveBeenCalledTimes(1) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows).toHaveLength(2) expect(rows[0]).toMatchObject({ @@ -54,8 +60,8 @@ describe('messages-dual-write', () => { model: null, streamId: null, }) - expect(rows[0].createdAt).toEqual(new Date(userMsg.timestamp)) - expect(rows[0].updatedAt).toEqual(new Date(userMsg.timestamp)) + expect(rows[0].createdAt as Date).toEqual(new Date(userMsg.timestamp)) + expect(rows[0].updatedAt as Date).toEqual(new Date(userMsg.timestamp)) expect(rows[1]).toMatchObject({ chatId: 'chat-1', @@ -63,13 +69,25 @@ describe('messages-dual-write', () => { role: 'assistant', content: assistantMsg, }) - expect(rows[1].createdAt).toEqual(new Date(assistantMsg.timestamp)) + expect(rows[1].createdAt as Date).toEqual(new Date(assistantMsg.timestamp)) }) - it('preserves per-message ordering via timestamp', async () => { + it('assigns seq as 0-based array index when the chat has no prior rows', async () => { + dbChainMockFns.where.mockResolvedValueOnce([{ maxSeq: null }]) + await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg]) - const rows = dbChainMockFns.values.mock.calls[0][0] - expect(rows[0].createdAt.getTime()).toBeLessThan(rows[1].createdAt.getTime()) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(0) + expect(rows[1].seq).toBe(1) + }) + + it('continues seq from MAX(seq)+1 when the chat already has rows', async () => { + dbChainMockFns.where.mockResolvedValueOnce([{ maxSeq: 4 }]) + + await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg]) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(5) + expect(rows[1].seq).toBe(6) }) it('passes chatModel and streamId options to every row', async () => { @@ -78,14 +96,14 @@ describe('messages-dual-write', () => { streamId: 'stream-xyz', }) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows[0].model).toBe('claude-sonnet-4-5') expect(rows[0].streamId).toBe('stream-xyz') expect(rows[1].model).toBe('claude-sonnet-4-5') expect(rows[1].streamId).toBe('stream-xyz') }) - it('uses ON CONFLICT DO UPDATE with chat_id + message_id target', async () => { + it('uses ON CONFLICT DO UPDATE that PRESERVES existing seq', async () => { await appendCopilotChatMessages('chat-1', [userMsg]) expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1) @@ -96,6 +114,14 @@ describe('messages-dual-write', () => { expect(conflictArg.set).toHaveProperty('model') expect(conflictArg.set).toHaveProperty('streamId') expect(conflictArg.set).toHaveProperty('updatedAt') + expect(conflictArg.set.seq.strings.join('')).toContain('COALESCE(') + }) + + it('collapses duplicate message ids to a single row', async () => { + await appendCopilotChatMessages('chat-1', [userMsg, { ...userMsg, content: 'dupe' }]) + const rows = lastValuesRows() + expect(rows).toHaveLength(1) + expect(rows[0].messageId).toBe('msg-user-1') }) it('swallows DB errors so the legacy JSONB write stays canonical', async () => { @@ -120,12 +146,9 @@ describe('messages-dual-write', () => { expect(dbChainMockFns.delete).toHaveBeenCalledTimes(1) expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows).toHaveLength(2) - expect(rows.map((r: { messageId: string }) => r.messageId)).toEqual([ - 'msg-user-1', - 'msg-asst-1', - ]) + expect(rows.map((r) => r.messageId)).toEqual(['msg-user-1', 'msg-asst-1']) expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1) const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0] @@ -133,12 +156,32 @@ describe('messages-dual-write', () => { expect(conflictArg.set).toHaveProperty('model') }) + it('assigns seq as the snapshot array index (0-based)', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg, assistantMsg]) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(0) + expect(rows[1].seq).toBe(1) + }) + + it('OVERWRITES seq on conflict so positions re-densify after a delete', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg]) + const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0] + expect(conflictArg.set.seq.strings.join('')).toBe('excluded.seq') + }) + + it('collapses duplicate message ids to a single row', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg, { ...userMsg, content: 'dupe' }]) + const rows = lastValuesRows() + expect(rows).toHaveLength(1) + expect(rows[0].seq).toBe(0) + }) + it('passes chatModel to every row in the snapshot', async () => { await replaceCopilotChatMessages('chat-1', [userMsg], { chatModel: 'gpt-4o-mini', }) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows[0].model).toBe('gpt-4o-mini') }) diff --git a/apps/sim/lib/copilot/chat/messages-dual-write.ts b/apps/sim/lib/copilot/chat/messages-dual-write.ts index 2a621d0b3c6..54e98afe3a0 100644 --- a/apps/sim/lib/copilot/chat/messages-dual-write.ts +++ b/apps/sim/lib/copilot/chat/messages-dual-write.ts @@ -7,9 +7,26 @@ import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' const logger = createLogger('CopilotMessagesDualWrite') +/** + * Keep the first occurrence of each message id. A single `INSERT ... ON + * CONFLICT` cannot touch the same conflict target twice, so a repeated id + * would otherwise throw. + */ +function dedupeById(messages: PersistedMessage[]): PersistedMessage[] { + const seen = new Set() + const out: PersistedMessage[] = [] + for (const m of messages) { + if (seen.has(m.id)) continue + seen.add(m.id) + out.push(m) + } + return out +} + function toRow( chatId: string, message: PersistedMessage, + seq: number, options?: { chatModel?: string | null; streamId?: string | null } ): typeof copilotMessages.$inferInsert { const ts = new Date(message.timestamp) @@ -18,6 +35,7 @@ function toRow( messageId: message.id, role: message.role, content: message, + seq, model: options?.chatModel ?? null, streamId: options?.streamId ?? null, createdAt: ts, @@ -27,8 +45,15 @@ function toRow( /** * Append messages to the new `copilot_messages` table. Best-effort — errors - * are logged but never thrown, since the legacy `copilot_chats.messages` - * JSONB column remains the source of truth during the dual-write rollout. + * are logged but never thrown; the legacy `copilot_chats.messages` JSONB + * column stays the source of truth during the dual-write rollout. + * + * `seq` is `MAX(seq) + index`, computed in JS (not in SQL, where every row of + * a multi-row INSERT would read the same pre-insert MAX and collide). The + * read-then-insert is non-atomic, so interleaved appends to one chat can tie + * `seq`; that window is bounded by the cutover read order (`seq, created_at, + * id`) and `replaceCopilotChatMessages`, which re-densifies `seq` from the + * authoritative JSONB order on the next snapshot save. */ export async function appendCopilotChatMessages( chatId: string, @@ -37,9 +62,15 @@ export async function appendCopilotChatMessages( ): Promise { if (messages.length === 0) return try { + const deduped = dedupeById(messages) + const [maxRow] = await db + .select({ maxSeq: sql`max(${copilotMessages.seq})` }) + .from(copilotMessages) + .where(eq(copilotMessages.chatId, chatId)) + const base = (maxRow?.maxSeq ?? -1) + 1 await db .insert(copilotMessages) - .values(messages.map((m) => toRow(chatId, m, options))) + .values(deduped.map((m, i) => toRow(chatId, m, base + i, options))) .onConflictDoUpdate({ target: [copilotMessages.chatId, copilotMessages.messageId], set: { @@ -47,6 +78,7 @@ export async function appendCopilotChatMessages( role: sql`excluded.role`, model: sql`COALESCE(excluded.model, ${copilotMessages.model})`, streamId: sql`COALESCE(excluded.stream_id, ${copilotMessages.streamId})`, + seq: sql`COALESCE(${copilotMessages.seq}, excluded.seq)`, updatedAt: sql`now()`, }, }) @@ -69,7 +101,8 @@ export async function replaceCopilotChatMessages( options?: { chatModel?: string | null } ): Promise { try { - const newMessageIds = messages.map((m) => m.id) + const deduped = dedupeById(messages) + const newMessageIds = deduped.map((m) => m.id) await db.transaction(async (tx) => { // Drop rows for messages not in the new snapshot. await tx @@ -82,12 +115,12 @@ export async function replaceCopilotChatMessages( ) : eq(copilotMessages.chatId, chatId) ) - if (messages.length === 0) return - // Upsert remaining rows. ON CONFLICT preserves existing stream_id / model - // so a snapshot save doesn't clobber metadata set during streaming. + if (deduped.length === 0) return + // Snapshot is authoritative on order, so seq = array index is overwritten + // on conflict; stream_id / model are preserved via COALESCE. await tx .insert(copilotMessages) - .values(messages.map((m) => toRow(chatId, m, options))) + .values(deduped.map((m, i) => toRow(chatId, m, i, options))) .onConflictDoUpdate({ target: [copilotMessages.chatId, copilotMessages.messageId], set: { @@ -95,6 +128,7 @@ export async function replaceCopilotChatMessages( role: sql`excluded.role`, model: sql`COALESCE(excluded.model, ${copilotMessages.model})`, streamId: sql`COALESCE(excluded.stream_id, ${copilotMessages.streamId})`, + seq: sql`excluded.seq`, updatedAt: sql`now()`, }, }) diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 21b6842cd6f..ea60844218a 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -639,6 +639,7 @@ async function processExecutionLogFromDb( .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, @@ -646,7 +647,7 @@ async function processExecutionLogFromDb( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, workflowName: workflow.name, }) .from(workflowExecutionLogs) @@ -671,6 +672,13 @@ async function processExecutionLogFromDb( } } + // Heavy execution data may live in object storage; resolve the pointer. + const { materializeExecutionData } = await import('@/lib/logs/execution/trace-store') + const executionData = (await materializeExecutionData( + log.executionData as Record | null, + { workspaceId: log.workspaceId, workflowId: log.workflowId, executionId: log.executionId } + )) as any + const summary = { id: log.id, workflowId: log.workflowId, @@ -681,13 +689,13 @@ async function processExecutionLogFromDb( endedAt: log.endedAt?.toISOString?.() || (log.endedAt ? String(log.endedAt) : null), totalDurationMs: log.totalDurationMs ?? null, workflowName: log.workflowName || '', - executionData: log.executionData + executionData: executionData ? { - traceSpans: (log.executionData as any).traceSpans || undefined, - errorDetails: (log.executionData as any).errorDetails || undefined, + traceSpans: executionData.traceSpans || undefined, + errorDetails: executionData.errorDetails || undefined, } : undefined, - cost: log.cost || undefined, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : undefined, } const content = JSON.stringify(summary) diff --git a/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts b/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts index f561d7b6190..10f18a192b9 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, desc, eq, type SQL } from 'drizzle-orm' import { GetExecutionSummary } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('GetExecutionSummaryServerTool') @@ -86,13 +87,14 @@ export const getExecutionSummaryServerTool: BaseServerTool< .select({ executionId: workflowExecutionLogs.executionId, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, workflowName: workflow.name, status: workflowExecutionLogs.status, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) @@ -101,26 +103,36 @@ export const getExecutionSummaryServerTool: BaseServerTool< .orderBy(desc(workflowExecutionLogs.startedAt)) .limit(clampedLimit) - const summaries: ExecutionSummary[] = rows.map((row) => { - const costData = row.cost as any - const errorMsg = row.level === 'error' ? extractErrorMessage(row.executionData) : null - - return { - executionId: row.executionId, - workflowId: row.workflowId, - workflowName: row.workflowName, - status: row.status, - trigger: row.trigger, - startedAt: row.startedAt.toISOString(), - durationMs: row.totalDurationMs ?? null, - cost: costData?.total ? Number(costData.total) : null, - error: errorMsg - ? typeof errorMsg === 'string' - ? errorMsg - : JSON.stringify(errorMsg) - : null, - } - }) + const summaries: ExecutionSummary[] = await Promise.all( + rows.map(async (row) => { + // Only externalized rows need a fetch; error fields live in the heavy data. + const executionData = + row.level === 'error' + ? await materializeExecutionData(row.executionData as Record | null, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + }) + : row.executionData + const errorMsg = row.level === 'error' ? extractErrorMessage(executionData) : null + + return { + executionId: row.executionId, + workflowId: row.workflowId, + workflowName: row.workflowName, + status: row.status, + trigger: row.trigger, + startedAt: row.startedAt.toISOString(), + durationMs: row.totalDurationMs ?? null, + cost: row.costTotal != null ? Number(row.costTotal) : null, + error: errorMsg + ? typeof errorMsg === 'string' + ? errorMsg + : JSON.stringify(errorMsg) + : null, + } + }) + ) logger.info('Execution summary prepared', { count: summaries.length, diff --git a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts index 0daf2aa07d0..471bc388057 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts @@ -5,6 +5,7 @@ import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { GetWorkflowLogs } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan } from '@/lib/logs/types' const logger = createLogger('GetWorkflowLogsServerTool') @@ -145,6 +146,8 @@ export const getWorkflowLogsServerTool: BaseServerTool { - const executionData = log.executionData as ExecutionData - const traceSpans = executionData?.traceSpans ?? [] - const blockExecutions = includeDetails - ? extractBlockExecutionsFromTraceSpans(traceSpans) - : [] - - const simplifiedBlocks: SimplifiedBlock[] = blockExecutions.map((block) => ({ - id: block.blockId, - name: block.blockName, - startedAt: block.startedAt, - endedAt: block.endedAt, - durationMs: block.durationMs, - output: block.outputData, - error: block.status === 'error' ? block.errorMessage : undefined, - })) - - const rawError = - executionData?.errorDetails?.error || - executionData?.errorDetails?.message || - executionData?.finalOutput?.error || - executionData?.error || - null - const errorMessage = rawError - ? typeof rawError === 'string' - ? rawError - : JSON.stringify(rawError) - : undefined - - return { - id: log.id, - executionId: log.executionId, - status: log.status, - startedAt: log.startedAt.toISOString(), - endedAt: log.endedAt ? log.endedAt.toISOString() : null, - durationMs: log.totalDurationMs ?? null, - ...(errorMessage ? { error: errorMessage } : {}), - ...(simplifiedBlocks.length > 0 ? { blocks: simplifiedBlocks } : {}), - } - }) + // Enforce the documented hard limit (the materialization fans out per row). + .limit(executionId ? 1 : Math.min(Math.max(1, limit), 3)) + + const simplifiedExecutions: SimplifiedExecution[] = await Promise.all( + executionLogs.map(async (log) => { + const executionData = (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as ExecutionData + const traceSpans = executionData?.traceSpans ?? [] + const blockExecutions = includeDetails + ? extractBlockExecutionsFromTraceSpans(traceSpans) + : [] + + const simplifiedBlocks: SimplifiedBlock[] = blockExecutions.map((block) => ({ + id: block.blockId, + name: block.blockName, + startedAt: block.startedAt, + endedAt: block.endedAt, + durationMs: block.durationMs, + output: block.outputData, + error: block.status === 'error' ? block.errorMessage : undefined, + })) + + const rawError = + executionData?.errorDetails?.error || + executionData?.errorDetails?.message || + executionData?.finalOutput?.error || + executionData?.error || + null + const errorMessage = rawError + ? typeof rawError === 'string' + ? rawError + : JSON.stringify(rawError) + : undefined + + return { + id: log.id, + executionId: log.executionId, + status: log.status, + startedAt: log.startedAt.toISOString(), + endedAt: log.endedAt ? log.endedAt.toISOString() : null, + durationMs: log.totalDurationMs ?? null, + ...(errorMessage ? { error: errorMessage } : {}), + ...(simplifiedBlocks.length > 0 ? { blocks: simplifiedBlocks } : {}), + } + }) + ) const resultSize = JSON.stringify(simplifiedExecutions).length logger.info('Workflow logs result prepared', { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index a3189f679d0..3b871859295 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -27,6 +27,8 @@ export const env = createEnv({ ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login BLOCKED_SIGNUP_DOMAINS: z.string().optional(), // Comma-separated list of email domains blocked from signing up (e.g., "gmail.com,yahoo.com") + SIGNUP_MX_VALIDATION_ENABLED: z.boolean().optional(), // Opt-in: validate the email's MX backend at signup (blocks no-MX domains and denylisted shared spam backends). Off by default; enable on hosted/abuse-targeted deployments. + BLOCKED_EMAIL_MX_HOSTS: z.string().optional(), // Comma-separated MX-host substrings blocked from signing up; matched against the domain's resolved MX backend to catch throwaway domains that share a mail backend. No defaults — operators supply their own list. Only used when SIGNUP_MX_VALIDATION_ENABLED is set. TRUSTED_ORIGINS: z.string().optional(), // Comma-separated additional origins to trust for auth (e.g., "https://app.example.com,https://www.example.com"). Merged into Better Auth trustedOrigins. TURNSTILE_SECRET_KEY: z.string().min(1).optional(), // Cloudflare Turnstile secret key for captcha verification SIGNUP_EMAIL_VALIDATION_ENABLED: z.boolean().optional(), // Enable disposable email blocking via better-auth-harmony (55K+ domains) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index f1d6e959f36..bde9252d652 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -81,6 +81,13 @@ export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED */ export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED) +/** + * Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam + * mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on + * self-hosted deployments with non-standard mail setups; enable on abuse-targeted deployments. + */ +export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED) + /** * Is Trigger.dev enabled for async job processing */ diff --git a/apps/sim/lib/core/utils/concurrency.ts b/apps/sim/lib/core/utils/concurrency.ts new file mode 100644 index 00000000000..9156c3c83d2 --- /dev/null +++ b/apps/sim/lib/core/utils/concurrency.ts @@ -0,0 +1,35 @@ +/** + * Maps over `items` with at most `limit` concurrent invocations of `fn`, + * preserving input order in the result. Use to bound fan-out (e.g. per-row + * object-storage reads) so a large batch doesn't issue every request at once. + * + * Contract: `fn` MUST NOT reject. Results are awaited via `Promise.all`, so a + * single rejection fails the entire batch (e.g. one bad row would break a whole + * logs export/list page). Callers that materialize per-row data pass a total + * mapper (one that catches and returns a degraded value rather than throwing); + * keep it that way. If a future caller needs per-item isolation, add an + * allSettled-style variant rather than letting a throwing mapper through here. + */ +export async function mapWithConcurrency( + items: readonly T[], + limit: number, + fn: (item: T, index: number) => Promise +): Promise { + const results = new Array(items.length) + const workerCount = Math.max(1, Math.min(limit, items.length)) + let cursor = 0 + + const worker = async (): Promise => { + while (true) { + const index = cursor++ + if (index >= items.length) return + results[index] = await fn(items[index], index) + } + } + + await Promise.all(Array.from({ length: workerCount }, worker)) + return results +} + +/** Default bound for per-row object-storage materialization fan-out. */ +export const MATERIALIZE_CONCURRENCY = 20 diff --git a/apps/sim/lib/data-drains/sources/workflow-logs.ts b/apps/sim/lib/data-drains/sources/workflow-logs.ts index 22487388f83..b64ce154791 100644 --- a/apps/sim/lib/data-drains/sources/workflow-logs.ts +++ b/apps/sim/lib/data-drains/sources/workflow-logs.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowExecutionLogs } from '@sim/db/schema' import { and, inArray, isNotNull } from 'drizzle-orm' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { decodeTimeCursor, encodeTimeCursor, @@ -9,6 +10,7 @@ import { } from '@/lib/data-drains/sources/cursor' import { getOrganizationWorkspaceIds } from '@/lib/data-drains/sources/helpers' import type { Cursor, DrainSource, SourcePageInput } from '@/lib/data-drains/types' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' type WorkflowLogRow = typeof workflowExecutionLogs.$inferSelect @@ -45,6 +47,22 @@ async function* pages(input: SourcePageInput): AsyncIterable { .limit(input.chunkSize) if (rows.length === 0) return + + // Heavy execution data may live in object storage; resolve pointers (bounded + // concurrency) so the drain exports full execution data, not the slim row. + // Use the order-preserving returned array (the util's documented contract) + // and write back, rather than mutating rows inside the mapper. + const materialized = await mapWithConcurrency(rows, MATERIALIZE_CONCURRENCY, (row) => + materializeExecutionData(row.executionData as Record | null, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + }) + ) + for (let i = 0; i < rows.length; i++) { + rows[i].executionData = materialized[i] as WorkflowLogRow['executionData'] + } + yield rows const last = rows[rows.length - 1] cursor = { ts: last.endedAt!.toISOString(), id: last.id } @@ -71,7 +89,8 @@ export const workflowLogsSource: DrainSource = { endedAt: row.endedAt ? row.endedAt.toISOString() : null, totalDurationMs: row.totalDurationMs, executionData: row.executionData, - cost: row.cost, + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: row.costTotal != null ? { total: Number(row.costTotal) } : null, files: row.files, createdAt: row.createdAt.toISOString(), } diff --git a/apps/sim/lib/execution/payloads/store.ts b/apps/sim/lib/execution/payloads/store.ts index f13af025eb4..d662447aaa8 100644 --- a/apps/sim/lib/execution/payloads/store.ts +++ b/apps/sim/lib/execution/payloads/store.ts @@ -31,6 +31,13 @@ export interface LargeValueStoreContext { userId?: string requireDurable?: boolean maxBytes?: number + /** + * When false, materialization does not register an execution_log reference for + * the key. Read-only consumers (e.g. viewing/exporting a completed log) set + * this: the value is already owned + referenced by its own execution, so + * re-registering on every read is wasteful and a needless failure point. + */ + trackReference?: boolean } function getKind(value: unknown): LargeValueKind { @@ -163,6 +170,11 @@ export async function storeLargeValue( const id = `lv_${generateShortId(12)}` let key = await persistValue(id, json, context) if (key) { + // Only clean up the uploaded object when registration definitively did NOT + // record ownership (returns false). If registration THROWS, the metadata + // state is uncertain (a row may have partially committed), so we propagate + // without deleting — deleting could orphan a metadata row pointing at a + // now-missing object. const registered = await registerPersistedValueOwner(key, size, referencedKeys, context) if (!registered) { await deleteUntrackedPersistedValue(key) @@ -204,16 +216,22 @@ export async function materializeLargeValueRef( return materializeLargeValueRefSync(ref, context) } - const { addLargeValueReference } = await import('@/lib/execution/payloads/large-value-metadata') - await addLargeValueReference( - { - workspaceId: context.workspaceId, - workflowId: context.workflowId, - executionId: context.executionId, - source: 'execution_log', - }, - ref.key - ) + if (context.trackReference !== false) { + const { addLargeValueReference } = await import('@/lib/execution/payloads/large-value-metadata') + // Reference tracking is GC-critical: if it fails, fail the read rather than + // return a value whose reference was never recorded (it could later be + // garbage-collected out from under a live consumer). Read-only consumers + // that don't need a reference set trackReference: false to skip this. + await addLargeValueReference( + { + workspaceId: context.workspaceId, + workflowId: context.workflowId, + executionId: context.executionId, + source: 'execution_log', + }, + ref.key + ) + } try { const cached = materializeLargeValueRefSync(ref, context) diff --git a/apps/sim/lib/logs/execution/logger.test.ts b/apps/sim/lib/logs/execution/logger.test.ts index 1cca0faca74..cf245552cc6 100644 --- a/apps/sim/lib/logs/execution/logger.test.ts +++ b/apps/sim/lib/logs/execution/logger.test.ts @@ -1,7 +1,37 @@ import { featureFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, test, vi } from 'vitest' +import { recordUsage } from '@/lib/billing/core/usage-log' import { ExecutionLogger } from '@/lib/logs/execution/logger' +const dbSelectMock = vi.hoisted(() => vi.fn()) +const dbExecuteMock = vi.hoisted(() => vi.fn()) +const txUpdateMock = vi.hoisted(() => + vi.fn(() => ({ set: () => ({ where: () => Promise.resolve() }) })) +) + +vi.mock('@sim/db', () => { + // The reconcile runs inside db.transaction with an advisory lock. The tx + // shares dbSelectMock so the existing call-order seeding (call 1 = workflow + // row via .limit, call 2 = already-billed via .groupBy) still applies; + // tx.execute (set_config + pg_advisory_xact_lock) is a no-op; tx.update backs + // the exact cost_total refine. + const tx = { + select: dbSelectMock, + insert: vi.fn(), + update: txUpdateMock, + execute: dbExecuteMock, + } + return { + db: { + select: dbSelectMock, + insert: vi.fn(), + update: vi.fn(), + execute: dbExecuteMock, + transaction: vi.fn(async (cb: (txArg: typeof tx) => Promise) => cb(tx)), + }, + } +}) + // Mock billing modules vi.mock('@/lib/billing/core/subscription', () => ({ getHighestPrioritySubscription: vi.fn(() => Promise.resolve(null)), @@ -19,6 +49,7 @@ vi.mock('@/lib/billing/core/usage', () => ({ vi.mock('@/lib/billing/core/usage-log', () => ({ recordUsage: vi.fn(() => Promise.resolve()), + stableEventKey: vi.fn((parts: Record) => JSON.stringify(parts)), })) vi.mock('@/lib/billing/threshold-billing', () => ({ @@ -439,3 +470,322 @@ describe('ExecutionLogger', () => { }) }) }) + +describe('recordExecutionUsage boundary-delta reconciliation', () => { + let logger: any + + beforeEach(() => { + logger = new ExecutionLogger() as any + vi.clearAllMocks() + }) + + const costSummary = (overrides: Record = {}) => ({ + totalCost: 0, + totalInputCost: 0, + totalOutputCost: 0, + totalTokens: 0, + totalPromptTokens: 0, + totalCompletionTokens: 0, + baseExecutionCharge: 0.005, + models: {}, + charges: {}, + ...overrides, + }) + + // db.select() is called twice in recordExecutionUsage: first the workflow row + // (terminated by .limit), then the already-billed usage_log rows (terminated + // by .groupBy). Return each in order. + const mockDb = (billedRows: Array>) => { + let call = 0 + dbSelectMock.mockImplementation(() => { + call += 1 + const rows = call === 1 ? [{ id: 'workflow-1', workspaceId: 'ws-1' }] : billedRows + const chain: any = { + from: () => chain, + where: () => chain, + limit: () => Promise.resolve(rows), + groupBy: () => Promise.resolve(rows), + } + return chain + }) + } + + const run = ( + summary: ReturnType, + billedRows: Array> + ) => { + mockDb(billedRows) + return logger.recordExecutionUsage('workflow-1', summary, 'api', 'exec-1', 'user-1') + } + + const lastEntries = () => vi.mocked(recordUsage).mock.calls[0][0].entries + + test('fresh completion records all targets (base fee + model) and returns the increment', async () => { + const recorded = await run( + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0.6, + output: 0.4, + tokens: { input: 10, output: 5, total: 15 }, + }, + }, + }), + [] + ) + + expect(recordUsage).toHaveBeenCalledTimes(1) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 1 }), + ]) + // Returns the amount recorded at this boundary (drives threshold-email math). + expect(recorded).toBeCloseTo(1.005, 8) + // cost_total is refined to the exact ledger sum inside the locked tx. + expect(txUpdateMock).toHaveBeenCalledTimes(1) + }) + + test('resume records only the increment over what is already billed', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { + total: 3, + input: 1.8, + output: 1.2, + tokens: { input: 30, output: 15, total: 45 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + + expect(recordUsage).toHaveBeenCalledTimes(1) + const entries = lastEntries() + expect(entries).toHaveLength(1) + expect(entries[0]).toEqual( + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 2 }) + ) + }) + + test('returns only the post-resume increment (not the cumulative total)', async () => { + const recorded = await run( + costSummary({ + models: { + 'gpt-4o': { + total: 3, + input: 0, + output: 0, + tokens: { input: 0, output: 0, total: 0 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + // Cumulative is 3.005, but only the $2 increment was recorded here — the + // threshold-email math must not re-count the pre-pause $1.005. + expect(recorded).toBe(2) + }) + + test('forwards a pre-resolved billing context to recordUsage (skips re-lookup)', async () => { + mockDb([]) + const billingContext = { + billingEntity: { type: 'user' as const, id: 'user-1' }, + billingPeriod: { start: new Date('2026-01-01'), end: new Date('2026-02-01') }, + } + await (logger as any).recordExecutionUsage( + 'workflow-1', + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0, + output: 0, + tokens: { input: 0, output: 0, total: 0 }, + }, + }, + }), + 'api', + 'exec-1', + 'user-1', + billingContext + ) + expect(vi.mocked(recordUsage).mock.calls[0][0]).toMatchObject({ + billingEntity: { type: 'user', id: 'user-1' }, + billingPeriod: billingContext.billingPeriod, + }) + }) + + test('retry with everything already billed records nothing (idempotent)', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0.6, + output: 0.4, + tokens: { input: 10, output: 5, total: 15 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + + expect(recordUsage).not.toHaveBeenCalled() + }) + + test('BYOK run records only the base fee (no zero-cost model rows)', async () => { + await run(costSummary({ models: {}, charges: {} }), []) + + expect(recordUsage).toHaveBeenCalledTimes(1) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + ]) + }) + + test('standalone hosted-tool charge reconciles as a tool row', async () => { + await run(costSummary({ charges: { 'Exa Search': { total: 0.02 } } }), [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + ]) + + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'tool', description: 'Exa Search', cost: 0.02 }), + ]) + }) + + test('two boundaries (pause then resume) bill the full run exactly once', async () => { + const model = (total: number) => ({ + 'gpt-4o': { + input: total * 0.6, + output: total * 0.4, + total, + tokens: { input: 10, output: 5, total: 15 }, + }, + }) + + // Boundary 1 (pause): nothing billed yet, partial cost. + await run(costSummary({ models: model(1) }), []) + const firstEntries = vi.mocked(recordUsage).mock.calls[0][0].entries + + // Feed boundary 1's rows back as already-billed for boundary 2. + const billedAfterFirst = firstEntries.map((e: any) => ({ + category: e.category, + description: e.description, + cost: String(e.cost), + })) + + // Boundary 2 (resume terminal): same model, higher cumulative cost. + await run(costSummary({ models: model(3) }), billedAfterFirst) + const secondEntries = vi.mocked(recordUsage).mock.calls[1][0].entries + + const ledgerTotal = [...firstEntries, ...secondEntries].reduce( + (sum: number, e: any) => sum + e.cost, + 0 + ) + expect(ledgerTotal).toBeCloseTo(3.005, 8) // base 0.005 once + gpt-4o 3 total + // Base fee billed once (boundary 1 only); model increment only at boundary 2. + expect(firstEntries.some((e: any) => e.category === 'fixed')).toBe(true) + expect(secondEntries.some((e: any) => e.category === 'fixed')).toBe(false) + expect(secondEntries).toEqual([ + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 2 }), + ]) + }) + + test('eventKey is scoped by billedBefore so cross-boundary increments do not collide', async () => { + const model = (total: number) => ({ + 'gpt-4o': { input: 0, output: 0, total, tokens: { input: 0, output: 0, total: 0 } }, + }) + + await run(costSummary({ models: model(1) }), []) + const key0 = vi + .mocked(recordUsage) + .mock.calls[0][0].entries.find((e: any) => e.category === 'model')?.eventKey + + await run(costSummary({ models: model(3) }), [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ]) + const key1 = vi + .mocked(recordUsage) + .mock.calls[1][0].entries.find((e: any) => e.category === 'model')?.eventKey + + expect(key0).toContain('"billedBefore":"0.00000000"') + expect(key1).toContain('"billedBefore":"1.00000000"') + expect(key0).not.toEqual(key1) + }) + + test('a decreased cumulative cost (negative delta) records nothing for that line', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 3, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '5' }, + ] + ) + expect(recordUsage).not.toHaveBeenCalled() + }) + + test('a model introduced only post-resume is billed in full; the already-billed model is skipped', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 1, tokens: { input: 0, output: 0, total: 0 } }, + 'claude-3': { input: 0, output: 0, total: 2, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'model', description: 'claude-3', cost: 2 }), + ]) + }) + + test('zero-cost models and charges (BYOK) are filtered out, leaving only the base fee', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 0, tokens: { input: 0, output: 0, total: 0 } }, + }, + charges: { Exa: { total: 0 } }, + }), + [] + ) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + ]) + }) + + test('reconciles inside a transaction holding a per-execution advisory lock', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 1, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [] + ) + + // set_config('lock_timeout') + pg_advisory_xact_lock both run on the tx. + expect(dbExecuteMock).toHaveBeenCalledTimes(2) + expect(recordUsage).toHaveBeenCalledTimes(1) + // The ledger INSERT participates in the locked transaction. + expect(vi.mocked(recordUsage).mock.calls[0][0]).toHaveProperty('tx') + }) +}) diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index e52b92f4276..66abe6283a1 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { member, + usageLog, userStats, user as userTable, workflow, @@ -9,15 +10,20 @@ import { import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { eq, sql } from 'drizzle-orm' -import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' +import { and, eq, sql } from 'drizzle-orm' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { checkUsageStatus, getOrgUsageLimit, maybeSendUsageThresholdEmail, } from '@/lib/billing/core/usage' -import { type ModelUsageMetadata, recordUsage } from '@/lib/billing/core/usage-log' +import { + type BillingContext, + deriveBillingContext, + type ModelUsageMetadata, + recordUsage, + stableEventKey, +} from '@/lib/billing/core/usage-log' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { redactApiKeys } from '@/lib/core/security/redaction' @@ -28,6 +34,7 @@ import { } from '@/lib/execution/payloads/large-value-metadata' import { emitWorkflowExecutionCompleted } from '@/lib/logs/events' import { snapshotService } from '@/lib/logs/execution/snapshot/service' +import { externalizeExecutionData, stripSpanCosts } from '@/lib/logs/execution/trace-store' import type { BlockOutputData, ExecutionEnvironment, @@ -48,6 +55,9 @@ const MAX_WORKFLOW_VALUE_BYTES = 512 * 1024 const EXECUTION_LOG_STATEMENT_TIMEOUT_MS = 30_000 const EXECUTION_LOG_LOCK_TIMEOUT_MS = 3_000 const EXECUTION_LOG_IDLE_TIMEOUT_MS = 5_000 +// Bounds the wait for the per-execution usage-reconcile advisory lock. Generous +// (favor waiting over dropping a charge); only trips on a pathological lock hold. +const USAGE_RECONCILE_LOCK_TIMEOUT_MS = 10_000 type ExecutionData = WorkflowExecutionLog['executionData'] @@ -548,13 +558,6 @@ export class ExecutionLogger implements IExecutionLoggerService { hasTraceSpans: false, traceSpanCount: 0, }, - cost: { - total: BASE_EXECUTION_CHARGE, - input: 0, - output: 0, - tokens: { input: 0, output: 0, total: 0 }, - models: {}, - }, }) .returning() @@ -590,7 +593,6 @@ export class ExecutionLogger implements IExecutionLoggerService { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models: Record< string, { @@ -601,6 +603,7 @@ export class ExecutionLogger implements IExecutionLoggerService { tokens: { input: number; output: number; total: number } } > + charges?: Record } finalOutput: BlockOutputData traceSpans?: TraceSpan[] @@ -732,7 +735,26 @@ export class ExecutionLogger implements IExecutionLoggerService { }, executionId ) - const completedExecutionLargeValueKeys = collectLargeValueReferenceKeys(completedExecutionData) + + stripSpanCosts((completedExecutionData as Record).traceSpans) + + // Externalization requires the execution owner (workspace_files.user_id is + // NOT NULL). billingUserId comes from environment.userId and is effectively + // always present for a real run; if it's somehow absent, keep data inline. + let storedExecutionData = completedExecutionData as Record + if (billingUserId) { + storedExecutionData = await externalizeExecutionData(storedExecutionData, { + workspaceId: existingLog?.workspaceId ?? null, + workflowId: existingLog?.workflowId ?? null, + executionId, + userId: billingUserId, + }) + } else { + execLog.warn('Skipping execution-data externalization: missing owner userId', { + executionId, + }) + } + const completedExecutionLargeValueKeys = collectLargeValueReferenceKeys(storedExecutionData) const updatedLog = await db.transaction(async (tx) => { await setExecutionLogWriteTimeouts(tx) @@ -745,8 +767,17 @@ export class ExecutionLogger implements IExecutionLoggerService { endedAt: new Date(endedAt), totalDurationMs: totalDuration, files: executionFiles.length > 0 ? executionFiles : null, - executionData: completedExecutionData, - cost: executionCost, + executionData: storedExecutionData, + // Faithful projection of the usage_log ledger. Neither cost_total nor + // models_used may regress below a prior boundary: a paused run that + // resumes into an empty-span error/cancel/cost-only fallback produces a + // base-only summary. GREATEST keeps the higher cumulative cost_total, + // and models_used is overwritten only when this boundary actually has + // models — so both stay == SUM(usage_log) on every monotonic path. + costTotal: sql`GREATEST(COALESCE(${workflowExecutionLogs.costTotal}, 0), ${costSummary.totalCost.toString()}::numeric)`, + ...(Object.keys(costSummary.models).length > 0 + ? { modelsUsed: Object.keys(costSummary.models) } + : {}), }) .where(eq(workflowExecutionLogs.executionId, executionId)) .returning() @@ -770,116 +801,146 @@ export class ExecutionLogger implements IExecutionLoggerService { }) try { - // Skip workflow lookup if workflow was deleted + // Skip workflow lookup if workflow was deleted. const wf = updatedLog.workflowId ? (await db.select().from(workflow).where(eq(workflow.id, updatedLog.workflowId)))[0] : undefined - if (wf && billingUserId) { - const [usr] = await db - .select({ id: userTable.id, email: userTable.email, name: userTable.name }) - .from(userTable) - .where(eq(userTable.id, billingUserId)) - .limit(1) - - if (usr?.email) { - const sub = await getHighestPrioritySubscription(usr.id) - - const costDelta = costSummary.totalCost - - const { getDisplayPlanName } = await import('@/lib/billing/plan-helpers') - const { isOrgScopedSubscription } = await import('@/lib/billing/subscriptions/utils') - const planName = getDisplayPlanName(sub?.plan) - const scope: 'user' | 'organization' = isOrgScopedSubscription(sub, usr.id) - ? 'organization' - : 'user' - - if (scope === 'user') { - const before = await checkUsageStatus(usr.id) - - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) - - const limit = before.usageData.limit - const percentBefore = before.usageData.percentUsed - const percentAfter = - limit > 0 ? Math.min(100, percentBefore + (costDelta / limit) * 100) : percentBefore - const currentUsageAfter = before.usageData.currentUsage + costDelta - - await maybeSendUsageThresholdEmail({ - scope: 'user', - userId: usr.id, - userEmail: usr.email, - userName: usr.name || undefined, - planName, - percentBefore, - percentAfter, - currentUsageAfter, - limit, - }) - } else if (sub?.referenceId) { - // Get org usage limit using shared helper - const { limit: orgLimit } = await getOrgUsageLimit(sub.referenceId, sub.plan, sub.seats) - - const [{ sum: orgUsageBefore }] = await db - .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, sub.referenceId)) - .limit(1) - const orgUsageBeforeNum = Number.parseFloat(String(orgUsageBefore ?? '0')) - - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) - const percentBefore = - orgLimit > 0 ? Math.min(100, (orgUsageBeforeNum / orgLimit) * 100) : 0 - const percentAfter = - orgLimit > 0 - ? Math.min(100, percentBefore + (costDelta / orgLimit) * 100) - : percentBefore - const currentUsageAfter = orgUsageBeforeNum + costDelta - - await maybeSendUsageThresholdEmail({ - scope: 'organization', - organizationId: sub.referenceId, - planName, - percentBefore, - percentAfter, - currentUsageAfter, - limit: orgLimit, - }) + const usr = + wf && billingUserId + ? ( + await db + .select({ id: userTable.id, email: userTable.email, name: userTable.name }) + .from(userTable) + .where(eq(userTable.id, billingUserId)) + .limit(1) + )[0] + : undefined + + // Resolve the billing context + the pre-increment usage snapshot for the + // threshold email BEFORE recording, so currentUsageAfter = before + + // costDelta doesn't double-count this boundary's own increment. + type EmailContext = + | { + scope: 'user' + userId: string + userEmail: string + userName: string | null + planName: string + before: Awaited> + } + | { + scope: 'organization' + organizationId: string + planName: string + orgLimit: number + orgUsageBefore: number + } + let billingContext: BillingContext | undefined + let emailContext: EmailContext | undefined + + if (usr?.email) { + const sub = await getHighestPrioritySubscription(usr.id) + // Derive the billing context once from the subscription we just fetched + // and thread it into recordExecutionUsage so recordUsage doesn't + // re-resolve the subscription on the hot completion path. + billingContext = deriveBillingContext(usr.id, sub) + + const { getDisplayPlanName } = await import('@/lib/billing/plan-helpers') + const { isOrgScopedSubscription } = await import('@/lib/billing/subscriptions/utils') + const planName = getDisplayPlanName(sub?.plan) + + if (isOrgScopedSubscription(sub, usr.id) && sub?.referenceId) { + const { limit: orgLimit } = await getOrgUsageLimit(sub.referenceId, sub.plan, sub.seats) + const [{ sum: orgBaselineSum }] = await db + .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) + .from(member) + .leftJoin(userStats, eq(member.userId, userStats.userId)) + .where(eq(member.organizationId, sub.referenceId)) + .limit(1) + // currentPeriodCost is only a baseline; add the org's attributed + // usage_log for the period so the threshold email reflects real usage. + const { getBillingPeriodUsageCost } = await import('@/lib/billing/core/usage-log') + const orgLedger = + sub.periodStart && sub.periodEnd + ? await getBillingPeriodUsageCost( + { type: 'organization', id: sub.referenceId }, + { start: sub.periodStart, end: sub.periodEnd } + ) + : 0 + emailContext = { + scope: 'organization', + organizationId: sub.referenceId, + planName, + orgLimit, + orgUsageBefore: Number.parseFloat(String(orgBaselineSum ?? '0')) + orgLedger, } } else { - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) + emailContext = { + scope: 'user', + userId: usr.id, + userEmail: usr.email, + userName: usr.name, + planName, + before: await checkUsageStatus(usr.id), + } } - } else { - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) + } + + // Record usage exactly once for every path. costDelta is the amount + // actually recorded at this boundary (the increment), not the cumulative + // run total — so resumed runs don't double-count pre-pause cost below. + const costDelta = await this.recordExecutionUsage( + updatedLog.workflowId, + costSummary, + updatedLog.trigger as ExecutionTrigger['type'], + executionId, + billingUserId, + billingContext + ) + + // Best-effort usage-threshold email. + if (emailContext?.scope === 'user') { + const limit = emailContext.before.usageData.limit + const percentBefore = emailContext.before.usageData.percentUsed + const percentAfter = + limit > 0 ? Math.min(100, percentBefore + (costDelta / limit) * 100) : percentBefore + const currentUsageAfter = emailContext.before.usageData.currentUsage + costDelta + + await maybeSendUsageThresholdEmail({ + scope: 'user', + userId: emailContext.userId, + userEmail: emailContext.userEmail, + userName: emailContext.userName || undefined, + planName: emailContext.planName, + percentBefore, + percentAfter, + currentUsageAfter, + limit, + }) + } else if (emailContext?.scope === 'organization') { + const { orgLimit, orgUsageBefore } = emailContext + const percentBefore = orgLimit > 0 ? Math.min(100, (orgUsageBefore / orgLimit) * 100) : 0 + const percentAfter = + orgLimit > 0 ? Math.min(100, percentBefore + (costDelta / orgLimit) * 100) : percentBefore + const currentUsageAfter = orgUsageBefore + costDelta + + await maybeSendUsageThresholdEmail({ + scope: 'organization', + organizationId: emailContext.organizationId, + planName: emailContext.planName, + percentBefore, + percentAfter, + currentUsageAfter, + limit: orgLimit, + }) } } catch (e) { + // Safety net: if a step above threw BEFORE the single record call, ensure + // the run is still billed. Reconciliation is idempotent, so re-recording + // after a successful call is a no-op. try { - await this.updateUserStats( + await this.recordExecutionUsage( updatedLog.workflowId, costSummary, updatedLog.trigger as ExecutionTrigger['type'], @@ -902,8 +963,13 @@ export class ExecutionLogger implements IExecutionLoggerService { startedAt: updatedLog.startedAt.toISOString(), endedAt: updatedLog.endedAt?.toISOString() || endedAt, totalDurationMs: updatedLog.totalDurationMs || totalDurationMs, - executionData: updatedLog.executionData as WorkflowExecutionLog['executionData'], - cost: updatedLog.cost as WorkflowExecutionLog['cost'], + // Return the full in-memory execution data (cost-stripped, with traceSpans + // and finalOutput), not the slim externalized row — downstream consumers + // (notification delivery, events) need the complete payload without an + // extra storage round-trip. + executionData: completedExecutionData as WorkflowExecutionLog['executionData'], + // From the in-memory cost summary (not the deprecated cost jsonb column). + cost: executionCost as WorkflowExecutionLog['cost'], createdAt: updatedLog.createdAt.toISOString(), } @@ -934,7 +1000,10 @@ export class ExecutionLogger implements IExecutionLoggerService { endedAt: workflowLog.endedAt?.toISOString() || workflowLog.startedAt.toISOString(), totalDurationMs: workflowLog.totalDurationMs || 0, executionData: workflowLog.executionData as WorkflowExecutionLog['executionData'], - cost: workflowLog.cost as WorkflowExecutionLog['cost'], + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: (workflowLog.costTotal != null + ? { total: Number(workflowLog.costTotal) } + : null) as WorkflowExecutionLog['cost'], createdAt: workflowLog.createdAt.toISOString(), } } @@ -959,7 +1028,7 @@ export class ExecutionLogger implements IExecutionLoggerService { return trimmedUserId.length > 0 ? trimmedUserId : null } - private async updateUserStats( + private async recordExecutionUsage( workflowId: string | null, costSummary: { totalCost: number @@ -969,7 +1038,6 @@ export class ExecutionLogger implements IExecutionLoggerService { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models?: Record< string, { @@ -980,28 +1048,31 @@ export class ExecutionLogger implements IExecutionLoggerService { tokens: { input: number; output: number; total: number } } > + charges?: Record }, trigger: ExecutionTrigger['type'], executionId?: string, - billingUserId?: string | null - ): Promise { + billingUserId?: string | null, + // Pre-resolved billing context. The completion path already fetches the + // subscription for usage-threshold emails; passing the derived context here + // lets recordUsage skip a redundant subscription lookup per completion. + billingContext?: BillingContext + ): Promise { const statsLog = logger.withMetadata({ workflowId: workflowId ?? undefined, executionId }) - if (!isBillingEnabled) { - statsLog.debug('Billing is disabled, skipping user stats cost update') - return - } - - if (costSummary.totalCost <= 0) { - statsLog.debug('No cost to update in user stats') - return - } + // The usage ledger (recordUsage below) is written regardless of + // BILLING_ENABLED so cost is available everywhere (incl. self-hosted). + // Only enforcement (overage/Stripe) is gated on the flag. + // Returns the amount actually recorded at THIS boundary (the increment), so + // callers drive usage-threshold math off the delta rather than the + // cumulative run total (which would double-count pre-pause cost on resume). if (!workflowId) { - statsLog.debug('Workflow was deleted, skipping user stats update') - return + statsLog.debug('Workflow was deleted, skipping usage recording') + return 0 } + let recordedIncrement = 0 try { const [workflowRecord] = await db .select() @@ -1010,43 +1081,47 @@ export class ExecutionLogger implements IExecutionLoggerService { .limit(1) if (!workflowRecord) { - statsLog.error('Workflow not found for user stats update') - return + statsLog.error('Workflow not found for usage recording') + return 0 } const userId = billingUserId?.trim() || null if (!userId) { - statsLog.error('Missing billing actor in execution context; skipping stats update', { + statsLog.error('Missing billing actor in execution context; skipping usage recording', { trigger, }) - return + return 0 } - const entries: Array<{ - category: 'model' | 'fixed' - source: 'workflow' + // Build the run's *cumulative* target ledger lines from the cost summary. + // The usage_log is then reconciled to these targets: at each completion + // boundary (pause or terminal) we record only the increment versus what + // is already billed for this execution. This bills the full run exactly + // once across pause/resume without double-charging on resume, and keeps + // pre-pause work billed even if the run is later abandoned. + type TargetLine = { + category: 'model' | 'fixed' | 'tool' description: string - cost: number + target: number metadata?: ModelUsageMetadata | null - }> = [] + } + const targets: TargetLine[] = [] if (costSummary.baseExecutionCharge > 0) { - entries.push({ + targets.push({ category: 'fixed', - source: 'workflow', description: 'execution_fee', - cost: costSummary.baseExecutionCharge, + target: costSummary.baseExecutionCharge, }) } if (costSummary.models) { for (const [modelName, modelData] of Object.entries(costSummary.models)) { if (modelData.total > 0) { - entries.push({ + targets.push({ category: 'model', - source: 'workflow', description: modelName, - cost: modelData.total, + target: modelData.total, metadata: { inputTokens: modelData.tokens.input, outputTokens: modelData.tokens.output, @@ -1058,23 +1133,173 @@ export class ExecutionLogger implements IExecutionLoggerService { } } - await recordUsage({ - userId, - entries, - workspaceId: workflowRecord.workspaceId ?? undefined, - workflowId, - executionId, - }) + // Non-model billable charges (standalone hosted-key tool/integration + // blocks). These derive from already-gated span costs in + // calculateCostSummary — BYOK'd tools produce no cost upstream, so they + // never create a row here. Recording them closes the standalone-tool gap + // so the ledger fully reconciles with the run total (no double charge: + // agent-embedded tool cost stays folded into its model row). + if (costSummary.charges) { + for (const [description, charge] of Object.entries(costSummary.charges)) { + if (charge.total > 0) { + targets.push({ category: 'tool', description, target: charge.total }) + } + } + } + + if (targets.length === 0) { + statsLog.debug('No cost to record') + return 0 + } + + // Matches the billedBefore key resolution (toFixed(8)): a delta below this + // is finer than the idempotency key can distinguish across boundaries, so + // ignoring it keeps the key and the gate consistent. + const COST_EPSILON = 1e-8 + + // Build the positive-increment ledger entries for a given already-billed + // snapshot. The eventKey is scoped by the already-billed-so-far amount so + // increments across boundaries never collide, while a retried boundary + // (same already-billed) dedups via onConflictDoNothing. + // + // Reconciliation keys on `description` (model name / tool name), which is + // the billing identity — DO NOT normalize or relabel it. Correctness + // across pause/resume relies on the same usage carrying the same + // description at every boundary; the paused snapshot retains each block's + // original model label, so pre-pause cost stays under its original key + // (delta 0 at terminal) and only genuinely new usage is charged. A future + // change that relabels historical spans would break this invariant. + const buildDeltaEntries = (alreadyBilled: Map) => { + const entries: Array<{ + category: 'model' | 'fixed' | 'tool' + source: 'workflow' + description: string + cost: number + eventKey: string + metadata?: ModelUsageMetadata | null + }> = [] + for (const line of targets) { + const billed = alreadyBilled.get(`${line.category}::${line.description}`) ?? 0 + const delta = line.target - billed + if (delta <= COST_EPSILON) continue + entries.push({ + category: line.category, + source: 'workflow', + description: line.description, + cost: delta, + eventKey: stableEventKey({ + executionId: executionId ?? '', + category: line.category, + description: line.description, + billedBefore: billed.toFixed(8), + }), + ...(line.metadata !== undefined ? { metadata: line.metadata } : {}), + }) + } + return entries + } + + if (executionId) { + // Serialize concurrent completion boundaries for this execution so the + // read-then-insert reconciliation cannot race. pg_advisory_xact_lock is + // transaction-scoped (auto-released on commit/rollback, pool-safe) and + // bounded by lock_timeout. The critical section is one SELECT + one + // INSERT; the lock is uncontended in the normal (already-serialized) + // flow and only matters under a cross-process double-completion of the + // same execution, where it stops a stale already-billed read from + // dropping the larger delta. + await db.transaction(async (tx) => { + await tx.execute( + sql`select set_config('lock_timeout', ${`${USAGE_RECONCILE_LOCK_TIMEOUT_MS}ms`}, true)` + ) + await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(${executionId}, 0))`) + + // Already-billed for this execution, scoped to the rows this path owns + // (source='workflow') so a same-executionId row from another source + // can't suppress a charge. + const billedRows = await tx + .select({ + category: usageLog.category, + description: usageLog.description, + cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where(and(eq(usageLog.executionId, executionId), eq(usageLog.source, 'workflow'))) + .groupBy(usageLog.category, usageLog.description) + + const alreadyBilled = new Map() + for (const row of billedRows) { + alreadyBilled.set( + `${row.category}::${row.description}`, + Number.parseFloat(row.cost ?? '0') + ) + } - // Check if user has hit overage threshold and bill incrementally - await checkAndBillOverageThreshold(userId) + const entries = buildDeltaEntries(alreadyBilled) + if (entries.length > 0) { + await recordUsage({ + userId, + entries, + workspaceId: workflowRecord.workspaceId ?? undefined, + workflowId, + executionId, + tx, + ...(billingContext ?? {}), + }) + recordedIncrement = entries.reduce((acc, e) => acc + e.cost, 0) + + // Refine cost_total to the EXACT post-reconciliation ledger sum, + // inside the same advisory-locked tx so it is atomic with the inserts + // and can't be clobbered by a concurrent boundary. Exact by + // construction: under the lock no delta collides, so the new sum is + // the prior workflow-source sum plus the deltas just inserted. This + // supersedes the main-transaction GREATEST baseline (which remains for + // early-return / no-executionId / failed-reconcile paths). + const ledgerSum = + [...alreadyBilled.values()].reduce((acc, v) => acc + v, 0) + recordedIncrement + await tx + .update(workflowExecutionLogs) + .set({ costTotal: ledgerSum.toString() }) + .where(eq(workflowExecutionLogs.executionId, executionId)) + } + }) + } else { + // No execution scope to reconcile/lock against (not expected at a + // workflow completion): record the full targets directly. + const entries = buildDeltaEntries(new Map()) + if (entries.length > 0) { + await recordUsage({ + userId, + entries, + workspaceId: workflowRecord.workspaceId ?? undefined, + workflowId, + ...(billingContext ?? {}), + }) + recordedIncrement = entries.reduce((acc, e) => acc + e.cost, 0) + } + } + + // Enforcement only when billing is enabled: the ledger above is always + // written, but overage/Stripe billing is gated on BILLING_ENABLED. + if (isBillingEnabled) { + await checkAndBillOverageThreshold(userId) + } } catch (error) { - statsLog.error('Error updating user stats with cost information', { - error, - costSummary, - }) - // Don't throw - we want execution to continue even if user stats update fails + // Swallowed so a billing-write failure never fails the execution. The + // reconciliation self-heals on a later boundary; a TERMINAL-boundary + // failure leaves the run under-billed (and cost_total may then exceed + // SUM(usage_log)), so log loudly enough to alert / reconcile out of band. + statsLog.error( + 'Failed to record execution usage to usage_log ledger; charge may be unbilled', + { + error, + billingUserId, + costSummary, + } + ) } + + return recordedIncrement } /** diff --git a/apps/sim/lib/logs/execution/logging-factory.test.ts b/apps/sim/lib/logs/execution/logging-factory.test.ts index 27194c4d913..7ec7fe11d19 100644 --- a/apps/sim/lib/logs/execution/logging-factory.test.ts +++ b/apps/sim/lib/logs/execution/logging-factory.test.ts @@ -150,7 +150,6 @@ describe('calculateCostSummary', () => { expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) expect(result.baseExecutionCharge).toBe(BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0) expect(result.totalInputCost).toBe(0) expect(result.totalOutputCost).toBe(0) expect(result.totalTokens).toBe(0) @@ -188,7 +187,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0.03) expect(result.totalInputCost).toBe(0.01) expect(result.totalOutputCost).toBe(0.02) expect(result.totalTokens).toBe(300) @@ -221,7 +219,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) expect(result.totalCost).toBe(0.033 + BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0.033) expect(result.totalInputCost).toBe(0.011) expect(result.totalOutputCost).toBe(0.022) expect(result.totalTokens).toBe(450) @@ -280,7 +277,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) expect(result.models['claude-3']).toBeDefined() expect(result.models['claude-3'].total).toBe(0.03) @@ -308,7 +304,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.models['gpt-4']).toBeDefined() }) @@ -345,7 +340,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(Object.keys(result.models)).toHaveLength(1) }) @@ -361,7 +355,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) // Should not add to models if model is not specified expect(Object.keys(result.models)).toHaveLength(0) @@ -427,7 +420,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0) expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) // Model is still tracked for token-usage display, but cost must be zero. expect(result.models['claude-opus-4-6'].total).toBe(0) @@ -462,7 +454,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) expect(result.models['gpt-4o'].total).toBe(0.03) }) @@ -494,9 +485,177 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.045) expect(result.totalCost).toBe(0.045 + BASE_EXECUTION_CHARGE) expect(result.models['gpt-4o'].total).toBe(0.045) expect(result.models['gpt-4o'].toolCost).toBe(0.015) }) + + test('records a standalone non-model billable span as a charge (closes the tool gap)', () => { + const traceSpans = [ + { + id: 'exa-block', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.charges['Exa Search']).toBeDefined() + expect(result.charges['Exa Search'].total).toBe(0.01) + expect(Object.keys(result.models)).toHaveLength(0) + // Ledger partition reconciles with the run total. + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('does not double-count: agent-embedded tool stays in the model row, not charges', () => { + const traceSpans = [ + { + id: 'agent-span', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.045, toolCost: 0.015 }, + tokens: { input: 1000, output: 2000, total: 3000 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(Object.keys(result.charges)).toHaveLength(0) + expect(result.models['gpt-4o'].total).toBe(0.045) + expect(result.models['gpt-4o'].toolCost).toBe(0.015) + }) + + test('mixed model + standalone tool run reconciles to total', () => { + const traceSpans = [ + { + id: 'agent', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + { + id: 'exa', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.charges['Exa Search'].total).toBe(0.01) + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('BYOK tool (no cost generated upstream) produces no charge row', () => { + const traceSpans = [ + { + id: 'exa-byok', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(Object.keys(result.charges)).toHaveLength(0) + expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) + }) + + test('does not double-count the synthetic workflow root (aggregate cost over leaves)', () => { + // buildTraceSpans wraps every run in a synthetic { type: 'workflow' } root + // whose cost.total is the SUM of its leaves. Counting that root in addition + // to the leaves double-charges the run — the root must be a pass-through. + const traceSpans = [ + { + id: 'workflow-execution', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.04 }, // == agent(0.03) + exa(0.01) + children: [ + { + id: 'agent-1', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + { + id: 'exa-1', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ], + }, + ] + + const result = calculateCostSummary(traceSpans) + + // The 0.04 root aggregate is NOT added on top of its leaves. + expect(result.charges['Workflow Execution']).toBeUndefined() + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.charges['Exa Search'].total).toBe(0.01) + expect(result.totalCost).toBeCloseTo(0.04 + BASE_EXECUTION_CHARGE, 10) + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('does not double-count nested sub-workflow roots', () => { + // A sub-workflow call nests another synthetic { type: 'workflow' } root + // (captureChildWorkflowLogs runs buildTraceSpans on the child). Both the + // outer root and the inner sub-workflow root carry aggregate costs; only the + // leaf agent inside should be billed. + const traceSpans = [ + { + id: 'workflow-execution', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.03 }, + children: [ + { + id: 'subworkflow-root', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.03 }, + children: [ + { + id: 'child-agent', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + ], + }, + ], + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.charges['Workflow Execution']).toBeUndefined() + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.totalCost).toBeCloseTo(0.03 + BASE_EXECUTION_CHARGE, 10) + }) }) diff --git a/apps/sim/lib/logs/execution/logging-factory.ts b/apps/sim/lib/logs/execution/logging-factory.ts index 9011a39189e..35699dde70c 100644 --- a/apps/sim/lib/logs/execution/logging-factory.ts +++ b/apps/sim/lib/logs/execution/logging-factory.ts @@ -87,20 +87,29 @@ export async function loadDeployedWorkflowStateForLogging( type CostTraceSpan = Pick & { type?: TraceSpan['type'] + name?: TraceSpan['name'] children?: CostTraceSpan[] } -type BillableTraceSpan = CostTraceSpan & { cost: NonNullable } - -function hasBillableCost(span: CostTraceSpan): span is BillableTraceSpan { - return span.cost !== undefined +export interface CostSummaryModel { + input: number + output: number + total: number + toolCost?: number + tokens: { input: number; output: number; total: number } } -function isModelBreakdownSpan(span: CostTraceSpan): boolean { - return span.type === 'model' +/** + * Non-model billable charge (e.g. a standalone hosted-key tool block such as + * Exa/Tavily/falai run outside an agent). These spans contribute to the run's + * total cost but carry no `model`, so they live here rather than in `models`. + * Summed per span name so the ledger has one row per integration. + */ +export interface CostSummaryCharge { + total: number } -export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { +export interface CostSummary { totalCost: number totalInputCost: number totalOutputCost: number @@ -108,18 +117,22 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number - models: Record< - string, - { - input: number - output: number - total: number - toolCost?: number - tokens: { input: number; output: number; total: number } - } - > -} { + models: Record + /** Non-model billable charges keyed by span name (tool/integration costs). */ + charges: Record +} + +type BillableTraceSpan = CostTraceSpan & { cost: NonNullable } + +function hasBillableCost(span: CostTraceSpan): span is BillableTraceSpan { + return span.cost !== undefined +} + +function isModelBreakdownSpan(span: CostTraceSpan): boolean { + return span.type === 'model' +} + +export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): CostSummary { if (!traceSpans || traceSpans.length === 0) { return { totalCost: BASE_EXECUTION_CHARGE, @@ -129,8 +142,8 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, models: {}, + charges: {}, } } @@ -152,19 +165,32 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { const costSpans: BillableTraceSpan[] = [] for (const span of spans) { + // `workflow`-typed spans are aggregate containers, not billable units: the + // synthetic "Workflow Execution" root (added to every run by + // buildTraceSpans) and any nested sub-workflow root carry a `cost.total` + // equal to the SUM of their descendants. Counting that aggregate in + // addition to the descendants double-charges the run, so treat these as + // pass-through: never count their own cost, always recurse into all + // children where the real billable leaves (agents, tools) live. + const isAggregateContainer = span.type === 'workflow' const hasOwnCost = hasBillableCost(span) - if (hasOwnCost) { + const countOwnCost = hasOwnCost && !isAggregateContainer + + if (countOwnCost) { costSpans.push(span) } if (span.children && Array.isArray(span.children)) { - if (hasOwnCost) { - // Parent already accounts for its model segments; only recurse into - // non-model children (e.g. nested workflow spans) to find further - // billable units. + if (countOwnCost) { + // Authoritative leaf (e.g. an agent block whose block-level cost is set + // by the provider response and already accounts for its model + // segments): only recurse into non-model children to find further + // standalone billable units, skipping the model-breakdown duplicates. const nonModelChildren = span.children.filter((child) => !isModelBreakdownSpan(child)) costSpans.push(...collectCostSpans(nonModelChildren)) } else { + // Container (workflow / sub-workflow root) or a no-cost parent: recurse + // into everything so nested billable leaves are counted exactly once. costSpans.push(...collectCostSpans(span.children)) } } @@ -181,16 +207,8 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { let totalTokens = 0 let totalPromptTokens = 0 let totalCompletionTokens = 0 - const models: Record< - string, - { - input: number - output: number - total: number - toolCost?: number - tokens: { input: number; output: number; total: number } - } - > = {} + const models: Record = {} + const charges: Record = {} for (const span of costSpans) { totalCost += span.cost.total || 0 @@ -220,10 +238,19 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { if (span.cost.toolCost) { models[model].toolCost = (models[model].toolCost || 0) + span.cost.toolCost } + } else if ((span.cost.total || 0) > 0) { + // Non-model billable span (e.g. a standalone hosted-key tool block). + // These previously contributed to the run total but were never itemized + // in the ledger (the "standalone tool gap"). Key by span name so each + // integration gets a single, reconciling charge row. + const description = span.name || span.type || 'tool' + if (!charges[description]) { + charges[description] = { total: 0 } + } + charges[description].total += span.cost.total || 0 } } - const modelCost = totalCost totalCost += BASE_EXECUTION_CHARGE return { @@ -234,7 +261,7 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens, totalCompletionTokens, baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost, models, + charges, } } diff --git a/apps/sim/lib/logs/execution/logging-session.test.ts b/apps/sim/lib/logs/execution/logging-session.test.ts index bdd338e1ff5..7ad138d0f93 100644 --- a/apps/sim/lib/logs/execution/logging-session.test.ts +++ b/apps/sim/lib/logs/execution/logging-session.test.ts @@ -75,7 +75,6 @@ vi.mock('@/lib/logs/execution/logging-factory', () => ({ totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: 0, - modelCost: 0, models: {}, }), createEnvironmentObject: vi.fn(), @@ -84,6 +83,7 @@ vi.mock('@/lib/logs/execution/logging-factory', () => ({ loadWorkflowStateForExecution: loadWorkflowStateForExecutionMock, })) +import { calculateCostSummary } from '@/lib/logs/execution/logging-factory' import { LoggingSession } from './logging-session' describe('LoggingSession start snapshots', () => { @@ -238,34 +238,54 @@ describe('LoggingSession completion retries', () => { ) }) - it('preserves accumulated cost during fallback completion', async () => { + it('derives fallback cost from trace spans when the primary completion fails', async () => { const session = new LoggingSession('workflow-1', 'execution-6', 'api', 'req-1') as any - session.accumulatedCost = { - total: 12, - input: 5, - output: 7, - tokens: { input: 11, output: 13, total: 24 }, - models: { - 'test-model': { - input: 5, - output: 7, - total: 12, - tokens: { input: 11, output: 13, total: 24 }, - }, - }, + // Resume-accumulation is retired: the cost-only fallback now derives its + // cost summary from the in-memory trace spans (billing itself reconciles + // from the usage_log ledger in recordExecutionUsage). The primary complete() + // path consumes one calculateCostSummary call before it fails, so queue the + // same value twice (primary attempt + fallback). + const spanCostSummary = { + totalCost: 12, + totalInputCost: 5, + totalOutputCost: 7, + totalTokens: 24, + totalPromptTokens: 11, + totalCompletionTokens: 13, + baseExecutionCharge: 0, + models: {}, + charges: {}, } - session.costFlushed = true + vi.mocked(calculateCostSummary) + .mockReturnValueOnce(spanCostSummary) + .mockReturnValueOnce(spanCostSummary) completeWorkflowExecutionMock .mockRejectedValueOnce(new Error('success finalize failed')) .mockResolvedValueOnce({}) - await expect(session.safeComplete({ finalOutput: { ok: true } })).resolves.toBeUndefined() + const traceSpans = [ + { + id: 'span-1', + name: 'Block A', + type: 'tool', + duration: 25, + startTime: '2026-03-13T10:00:00.000Z', + endTime: '2026-03-13T10:00:00.025Z', + status: 'success', + }, + ] as any + + await expect( + session.safeComplete({ finalOutput: { ok: true }, traceSpans }) + ).resolves.toBeUndefined() + expect(calculateCostSummary).toHaveBeenLastCalledWith(traceSpans) expect(completeWorkflowExecutionMock).toHaveBeenLastCalledWith( expect.objectContaining({ executionId: 'execution-6', + finalizationPath: 'fallback_completed', costSummary: expect.objectContaining({ totalCost: 12, totalInputCost: 5, @@ -440,24 +460,21 @@ describe('LoggingSession completion retries', () => { expect(session.complete).toHaveBeenCalledTimes(1) }) - it('drains fire-and-forget cost flushes before terminal completion', async () => { - let releaseFlush: (() => void) | undefined - const flushPromise = new Promise((resolve) => { - releaseFlush = resolve + it('drains fire-and-forget block-complete marker writes before terminal completion', async () => { + let releasePersist: (() => void) | undefined + const persistPromise = new Promise((resolve) => { + releasePersist = resolve }) const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1') as any - session.flushAccumulatedCost = vi.fn(() => flushPromise) + session.persistLastCompletedBlock = vi.fn(() => persistPromise) session.complete = vi.fn().mockResolvedValue(undefined) - await session.onBlockComplete('block-2', 'Transform', 'function', { + // onBlockComplete is now marker-only; its marker write is fire-and-forget + // but tracked, so terminal completion must drain it first. + void session.onBlockComplete('block-2', 'Transform', 'function', { endedAt: '2025-01-01T00:00:01.000Z', - output: { - value: true, - cost: { total: 1, input: 1, output: 0 }, - tokens: { input: 1, output: 0, total: 1 }, - model: 'test-model', - }, + output: { value: true }, }) const completionPromise = session.safeComplete({ finalOutput: { ok: true } }) @@ -466,11 +483,11 @@ describe('LoggingSession completion retries', () => { expect(session.complete).not.toHaveBeenCalled() - releaseFlush?.() + releasePersist?.() await completionPromise - expect(session.flushAccumulatedCost).toHaveBeenCalledTimes(1) + expect(session.persistLastCompletedBlock).toHaveBeenCalledTimes(1) expect(session.complete).toHaveBeenCalledTimes(1) }) diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 56eb463e201..09bfd2348ca 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -3,7 +3,6 @@ import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, sql } from 'drizzle-orm' -import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { executionLogger } from '@/lib/logs/execution/logger' import { calculateCostSummary, @@ -118,22 +117,6 @@ export interface SessionPausedParams { workflowInput?: any } -interface AccumulatedCost { - total: number - input: number - output: number - tokens: { input: number; output: number; total: number } - models: Record< - string, - { - input: number - output: number - total: number - tokens: { input: number; output: number; total: number } - } - > -} - export class LoggingSession { private workflowId: string private executionId: string @@ -151,15 +134,7 @@ export class LoggingSession { private completionPromise: Promise | null = null private completionAttempt: CompletionAttempt | null = null private completionAttemptFailed = false - private accumulatedCost: AccumulatedCost = { - total: BASE_EXECUTION_CHARGE, - input: 0, - output: 0, - tokens: { input: 0, output: 0, total: 0 }, - models: {}, - } private pendingProgressWrites = new Set>() - private costFlushed = false private postExecutionPromise: Promise | null = null constructor( @@ -249,7 +224,6 @@ export class LoggingSession { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models: Record< string, { @@ -259,6 +233,9 @@ export class LoggingSession { tokens: { input: number; output: number; total: number } } > + // Non-model billable charges (standalone tool/integration costs). Carried + // through so the partition can't be silently dropped at this boundary. + charges?: Record } finalOutput: Record traceSpans: TraceSpan[] @@ -292,46 +269,9 @@ export class LoggingSession { blockType: string, output: any ): Promise { - // Accumulate cost synchronously before any await so that fire-and-forget - // callers still capture the full cost even if DB writes are not awaited. - const blockOutput = output?.output - if ( - blockOutput?.cost && - typeof blockOutput.cost.total === 'number' && - blockOutput.cost.total > 0 - ) { - const { cost, tokens, model } = blockOutput - - this.accumulatedCost.total += cost.total || 0 - this.accumulatedCost.input += cost.input || 0 - this.accumulatedCost.output += cost.output || 0 - - if (tokens) { - this.accumulatedCost.tokens.input += tokens.input || 0 - this.accumulatedCost.tokens.output += tokens.output || 0 - this.accumulatedCost.tokens.total += tokens.total || 0 - } - - if (model) { - if (!this.accumulatedCost.models[model]) { - this.accumulatedCost.models[model] = { - input: 0, - output: 0, - total: 0, - tokens: { input: 0, output: 0, total: 0 }, - } - } - this.accumulatedCost.models[model].input += cost.input || 0 - this.accumulatedCost.models[model].output += cost.output || 0 - this.accumulatedCost.models[model].total += cost.total || 0 - if (tokens) { - this.accumulatedCost.models[model].tokens.input += tokens.input || 0 - this.accumulatedCost.models[model].tokens.output += tokens.output || 0 - this.accumulatedCost.models[model].tokens.total += tokens.total || 0 - } - } - } - + // Cost is recorded into the usage_log ledger and reconciled at completion + // boundaries (see recordExecutionUsage); onBlockComplete only persists the + // last-completed-block progress marker. await this.trackProgressWrite( this.persistLastCompletedBlock({ blockId, @@ -341,76 +281,6 @@ export class LoggingSession { success: !output?.output?.error, }) ) - - if ( - blockOutput?.cost && - typeof blockOutput.cost.total === 'number' && - blockOutput.cost.total > 0 - ) { - void this.trackProgressWrite(this.flushAccumulatedCost()) - } - } - - private async flushAccumulatedCost(): Promise { - try { - await db - .update(workflowExecutionLogs) - .set({ - cost: { - total: this.accumulatedCost.total, - input: this.accumulatedCost.input, - output: this.accumulatedCost.output, - tokens: this.accumulatedCost.tokens, - models: this.accumulatedCost.models, - }, - }) - .where( - and( - eq(workflowExecutionLogs.workflowId, this.workflowId), - eq(workflowExecutionLogs.executionId, this.executionId) - ) - ) - - this.costFlushed = true - } catch (error) { - logger.error(`Failed to flush accumulated cost for execution ${this.executionId}:`, { - error: toError(error).message, - }) - } - } - - private async loadExistingCost(): Promise { - try { - const [existing] = await db - .select({ cost: workflowExecutionLogs.cost }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, this.workflowId), - eq(workflowExecutionLogs.executionId, this.executionId) - ) - ) - .limit(1) - - if (existing?.cost) { - const cost = existing.cost as AccumulatedCost - this.accumulatedCost = { - total: cost.total || BASE_EXECUTION_CHARGE, - input: cost.input || 0, - output: cost.output || 0, - tokens: { - input: cost.tokens?.input || 0, - output: cost.tokens?.output || 0, - total: cost.tokens?.total || 0, - }, - models: cost.models || {}, - } - } - } catch (error) { - logger.error(`Failed to load existing cost for execution ${this.executionId}:`, { - error: toError(error).message, - }) - } } async start(params: SessionStartParams): Promise { @@ -451,8 +321,9 @@ export class LoggingSession { deploymentVersionId, }) } else { + // Resume: no cost reload needed. Billing reconciles from the usage_log + // ledger (pre-pause rows already exist) plus the live cost summary. this.isResume = true - await this.loadExistingCost() } } catch (error) { if (this.requestId) { @@ -577,6 +448,8 @@ export class LoggingSession { const hasProvidedSpans = Array.isArray(traceSpans) && traceSpans.length > 0 + // calculateCostSummary([]) / (undefined) already returns the base-charge + // summary, so the no-spans branch needs no separate literal. const costSummary = skipCost ? { totalCost: 0, @@ -586,22 +459,10 @@ export class LoggingSession { totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: 0, - modelCost: 0, models: {}, + charges: {}, } - : hasProvidedSpans - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + : calculateCostSummary(traceSpans) const message = error?.message || 'Run failed before starting blocks' @@ -710,19 +571,9 @@ export class LoggingSession { return } - const costSummary = traceSpans?.length - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // calculateCostSummary handles empty/undefined spans by returning the + // base-charge summary, so no separate no-spans literal is needed. + const costSummary = calculateCostSummary(traceSpans) await this.completeExecutionWithFinalization({ endedAt: endTime.toISOString(), @@ -814,19 +665,9 @@ export class LoggingSession { return } - const costSummary = traceSpans?.length - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // calculateCostSummary handles empty/undefined spans by returning the + // base-charge summary, so no separate no-spans literal is needed. + const costSummary = calculateCostSummary(traceSpans) await this.completeExecutionWithFinalization({ endedAt: endTime.toISOString(), @@ -1185,37 +1026,12 @@ export class LoggingSession { ) try { - const hasAccumulatedCost = - this.costFlushed || - this.accumulatedCost.total > BASE_EXECUTION_CHARGE || - this.accumulatedCost.tokens.total > 0 || - Object.keys(this.accumulatedCost.models).length > 0 - - const costSummary = hasAccumulatedCost - ? { - totalCost: this.accumulatedCost.total, - totalInputCost: this.accumulatedCost.input, - totalOutputCost: this.accumulatedCost.output, - totalTokens: this.accumulatedCost.tokens.total, - totalPromptTokens: this.accumulatedCost.tokens.input, - totalCompletionTokens: this.accumulatedCost.tokens.output, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: Math.max(0, this.accumulatedCost.total - BASE_EXECUTION_CHARGE), - models: this.accumulatedCost.models, - } - : params.traceSpans?.length - ? calculateCostSummary(params.traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // Billing is reconciled from the usage_log ledger in recordExecutionUsage; + // here we only need a cost summary to compute the run total. Derive it + // from the in-memory trace spans when available (this fallback fires when + // persisting spans failed, not when computing them did), else just the + // base execution charge. + const costSummary = calculateCostSummary(params.traceSpans) const finalOutput = params.finalOutput || { _fallback: true, error: params.errorMessage } diff --git a/apps/sim/lib/logs/execution/trace-store.ts b/apps/sim/lib/logs/execution/trace-store.ts new file mode 100644 index 00000000000..83312eb1ec2 --- /dev/null +++ b/apps/sim/lib/logs/execution/trace-store.ts @@ -0,0 +1,179 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref' +import { materializeLargeValueRef, storeLargeValue } from '@/lib/execution/payloads/store' + +const logger = createLogger('TraceStore') + +/** + * Key under which the externalized-execution-data pointer (a `__simLargeValueRef`) + * is stored on the slim `execution_data` row. + */ +export const TRACE_STORE_REF_KEY = 'traceStoreRef' + +/** + * The only metadata kept inline on the slim row (everything else lives in the + * externalized object). These two describe trace presence/count and uniquely + * survive object expiry — so a reader can still report "trace data expired (N + * spans)" after retention without an object fetch. All other fields + * (environment, trigger, tokens, models, truncation flags, and of course the + * heavy payloads) are in the stored object and recovered on materialize, so + * keeping them inline too would just be duplication. + */ +const INLINE_MARKER_KEYS = ['hasTraceSpans', 'traceSpanCount'] as const + +/** + * Read-path context. Resolves an externalized payload by storage key, authorized + * via the (already-authorized) workspace — no owner needed. + */ +interface TraceStoreReadContext { + workspaceId: string | null + workflowId: string | null + executionId: string +} + +/** + * Write-path context. Requires the execution owner's `userId`: the externalized + * object is tracked in `workspace_files`, whose `user_id` column is NOT NULL + * (FK -> user.id). Requiring it here makes "a write needs an owner" a + * compile-time invariant, so callers must resolve the owner before persisting. + */ +interface TraceStoreWriteContext extends TraceStoreReadContext { + userId: string +} + +/** + * Recovers the workflowId embedded in a large-value storage key + * (`execution/{workspaceId}/{workflowId}/{executionId}/`). Used when the + * log row's workflowId has been nulled by workflow deletion. + */ +function workflowIdFromStorageKey(key: string | undefined): string | undefined { + if (!key) return undefined + const parts = key.split('/') + return parts.length >= 5 && parts[0] === 'execution' ? parts[2] : undefined +} + +/** + * Recursively removes `cost` from trace spans before persistence. Cost lives in + * exactly one place — the usage_log ledger — so persisted spans carry only + * structure, timing, and tokens (KTD7). Must run AFTER `calculateCostSummary` + * has consumed span costs in memory. + */ +export function stripSpanCosts(spans: unknown): void { + if (!Array.isArray(spans)) return + for (const span of spans) { + if (!span || typeof span !== 'object') continue + const record = span as { cost?: unknown; children?: unknown } + if ('cost' in record) record.cost = undefined + if (Array.isArray(record.children)) stripSpanCosts(record.children) + } +} + +/** + * Externalizes heavy `execution_data` to object storage as a single large value + * (reusing the execution-context large-value store + its reference/dependency/GC + * machinery — KTD4/KTD8), returning a slim row payload that keeps inline markers + * plus the `__simLargeValueRef` pointer. + * + * On any failure (no scope, oversized, storage error) the original (already + * cost-stripped) execution data is returned unchanged so the log is never lost. + */ +export async function externalizeExecutionData( + executionData: Record, + context: TraceStoreWriteContext +): Promise> { + const { workspaceId, workflowId, executionId, userId } = context + // workspaceId/workflowId build the storage key and can be null for + // deleted-workflow rows. userId is type-guaranteed by TraceStoreWriteContext; + // the falsy check is a defensive guard against an empty string. If any are + // missing the durable write can't succeed, so keep the data inline. + if (!workspaceId || !workflowId || !userId) return executionData + + try { + const json = JSON.stringify(executionData) + const size = Buffer.byteLength(json, 'utf8') + + // storeLargeValue persists to the execution bucket with a conforming key and + // registers owner + dependency closure (trace -> nested span large values), + // so GC keeps nested children alive while this run's log row exists. + const ref = await storeLargeValue(executionData, json, size, { + workspaceId, + workflowId, + executionId, + userId, + requireDurable: true, + }) + + const { preview: _preview, ...slimRef } = ref + + const slim: Record = { [TRACE_STORE_REF_KEY]: slimRef } + for (const key of INLINE_MARKER_KEYS) { + if (key in executionData) slim[key] = executionData[key] + } + return slim + } catch (error) { + logger.warn('Failed to externalize execution data; keeping inline', { + executionId, + error: toError(error).message, + }) + return executionData + } +} + +/** + * Resolves an `execution_data` row into its full form for reads. When the row + * carries a trace-store pointer, the payload is materialized from storage and + * merged with the inline markers; otherwise the row is returned unchanged + * (inline / pre-externalization runs). One level only — nested span + * `__simLargeValueRef` stubs remain as previews, matching prior behavior. + * + * Returns metadata-only (the slim row minus the pointer) if the object is + * missing/unreadable (e.g. post-retention) so reads degrade rather than crash. + */ +export async function materializeExecutionData( + executionData: Record | null | undefined, + context: TraceStoreReadContext +): Promise> { + if (!executionData) return {} + + const ref = executionData[TRACE_STORE_REF_KEY] + if (!isLargeValueRef(ref)) return executionData + + const { [TRACE_STORE_REF_KEY]: _pointer, ...markers } = executionData + + if (!context.workspaceId) return markers + + // workflowId is `set null` on workflow delete, but the ref key embeds the + // original workflowId — recover it so deleted-workflow logs stay readable. + // Workspace authorization still comes from the (authorized) caller context. + const workflowId = context.workflowId ?? workflowIdFromStorageKey(ref.key) + if (!workflowId) return markers + + try { + const materialized = await materializeLargeValueRef(ref, { + workspaceId: context.workspaceId, + workflowId, + executionId: context.executionId, + maxBytes: ref.size, + // Read-only: the value is already referenced by its own execution; don't + // re-register (or fail) on every view/export. + trackReference: false, + }) + + if (!materialized || typeof materialized !== 'object') { + logger.warn('Trace store object unavailable; returning metadata only', { + executionId: context.executionId, + key: ref.key, + }) + return markers + } + + return { ...(materialized as Record), ...markers } + } catch (error) { + logger.warn('Failed to materialize execution data; returning metadata only', { + executionId: context.executionId, + error: toError(error).message, + }) + return markers + } +} diff --git a/apps/sim/lib/logs/fetch-log-detail.ts b/apps/sim/lib/logs/fetch-log-detail.ts index 1a5aea4dc26..e1cef8728d2 100644 --- a/apps/sim/lib/logs/fetch-log-detail.ts +++ b/apps/sim/lib/logs/fetch-log-detail.ts @@ -3,14 +3,69 @@ import { jobExecutionLogs, pausedExecutions, permissions, + usageLog, workflow, workflowDeploymentVersion, workflowExecutionLogs, } from '@sim/db/schema' import { and, eq, type SQL } from 'drizzle-orm' +import type { CostLedger } from '@/lib/api/contracts/logs' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' type LookupColumn = 'id' | 'executionId' +async function buildCostLedger(executionId: string): Promise { + const rows = await db + .select({ + category: usageLog.category, + description: usageLog.description, + cost: usageLog.cost, + metadata: usageLog.metadata, + }) + .from(usageLog) + .where(and(eq(usageLog.executionId, executionId), eq(usageLog.source, 'workflow'))) + + if (rows.length === 0) return null + + type LedgerItem = CostLedger['items'][number] + const byKey = new Map() + for (const row of rows) { + const metadata = (row.metadata ?? {}) as { inputTokens?: number; outputTokens?: number } + const category = row.category as LedgerItem['category'] + const key = `${category}::${row.description}` + const existing = byKey.get(key) + if (existing) { + existing.cost += Number(row.cost) + if (typeof metadata.inputTokens === 'number') { + existing.inputTokens = Math.max(existing.inputTokens ?? 0, metadata.inputTokens) + } + if (typeof metadata.outputTokens === 'number') { + existing.outputTokens = Math.max(existing.outputTokens ?? 0, metadata.outputTokens) + } + } else { + byKey.set(key, { + category, + description: row.description, + cost: Number(row.cost), + ...(typeof metadata.inputTokens === 'number' ? { inputTokens: metadata.inputTokens } : {}), + ...(typeof metadata.outputTokens === 'number' + ? { outputTokens: metadata.outputTokens } + : {}), + }) + } + } + + const items = [...byKey.values()] + const total = items.reduce((sum, item) => sum + item.cost, 0) + return { total, items } +} + +export function jobCostTotal(raw: unknown): { total: number } | null { + const total = (raw as { total?: unknown } | null | undefined)?.total + const n = total == null ? Number.NaN : Number(total) + return Number.isFinite(n) ? { total: n } : null +} + interface FetchLogDetailArgs { userId: string workspaceId: string @@ -47,7 +102,7 @@ export async function fetchLogDetail({ endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, @@ -105,6 +160,18 @@ export async function fetchLogDetail({ (totalPauseCount > 0 && resumedCount < totalPauseCount) || (log.pausedStatus !== null && log.pausedStatus !== 'fully_resumed') + // Cost is sourced exclusively from the usage_log ledger (itemized breakdown) + // and its cost_total projection (run total). The cost jsonb is never read. + const costLedger = await buildCostLedger(log.executionId) + const totalDollars = costLedger?.total ?? (log.costTotal != null ? Number(log.costTotal) : null) + + // Trace spans / heavy execution data may live in object storage; resolve the + // pointer here (no-op for inline / pre-externalization rows). + const executionData = await materializeExecutionData( + log.executionData as Record | null, + { workspaceId, workflowId: log.workflowId, executionId: log.executionId } + ) + return { id: log.id, workflowId: log.workflowId, @@ -119,7 +186,8 @@ export async function fetchLogDetail({ createdAt: log.startedAt.toISOString(), workflow: workflowSummary, jobTitle: null, - cost: log.cost ?? null, + cost: totalDollars != null ? { total: totalDollars } : null, + costLedger, pauseSummary: { status: log.pausedStatus ?? null, total: totalPauseCount, @@ -128,7 +196,7 @@ export async function fetchLogDetail({ hasPendingPause, executionData: { totalDuration: log.totalDurationMs, - ...((log.executionData as Record | null) ?? {}), + ...executionData, enhanced: true as const, }, files: log.files ?? null, @@ -184,7 +252,7 @@ export async function fetchLogDetail({ createdAt: jobLog.startedAt.toISOString(), workflow: null, jobTitle: ((execData.trigger as Record | undefined)?.source as string) ?? null, - cost: jobLog.cost ?? null, + cost: jobCostTotal(jobLog.cost), pauseSummary: { status: null, total: 0, resumed: 0 }, hasPendingPause: false, executionData: { diff --git a/apps/sim/lib/logs/filters.ts b/apps/sim/lib/logs/filters.ts index cb2e1dccf29..0569e535a5c 100644 --- a/apps/sim/lib/logs/filters.ts +++ b/apps/sim/lib/logs/filters.ts @@ -195,7 +195,8 @@ function buildSearchConditions(params: { } function buildCostCondition(operator: ComparisonOperator, value: number): SQL { - const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric` + // Indexed projection of the usage_log ledger (dollars); no live aggregation. + const costField = sql`${workflowExecutionLogs.costTotal}` switch (operator) { case '=': diff --git a/apps/sim/lib/mcp/copilot-deprecated.ts b/apps/sim/lib/mcp/copilot-deprecated.ts new file mode 100644 index 00000000000..a3be7e7ebe6 --- /dev/null +++ b/apps/sim/lib/mcp/copilot-deprecated.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server' + +const DEPRECATION_MESSAGE = 'Copilot MCP has been deprecated and is no longer available.' + +/** + * Standard 410 response for the deprecated Copilot MCP surface. Used by the + * copilot MCP resource route and its copilot-specific OAuth discovery routes. + */ +export function copilotMcpDeprecatedResponse(): NextResponse { + return NextResponse.json( + { error: 'gone', message: DEPRECATION_MESSAGE }, + { + status: 410, + headers: { 'Cache-Control': 'no-store' }, + } + ) +} + +/** + * JSON-RPC flavored 410 response for the deprecated Copilot MCP `POST` endpoint, + * so MCP clients surface a clean error envelope instead of an opaque body. + */ +export function copilotMcpDeprecatedJsonRpcResponse(): NextResponse { + return NextResponse.json( + { + jsonrpc: '2.0', + id: null, + error: { code: -32000, message: DEPRECATION_MESSAGE }, + }, + { + status: 410, + headers: { 'Cache-Control': 'no-store' }, + } + ) +} diff --git a/apps/sim/lib/messaging/email/validation.server.test.ts b/apps/sim/lib/messaging/email/validation.server.test.ts new file mode 100644 index 00000000000..9fcfb4de6d4 --- /dev/null +++ b/apps/sim/lib/messaging/email/validation.server.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockResolveMx, envRef } = vi.hoisted(() => ({ + mockResolveMx: vi.fn(), + envRef: { + BLOCKED_EMAIL_MX_HOSTS: undefined as string | undefined, + }, +})) + +vi.mock('dns/promises', () => ({ + default: { resolveMx: mockResolveMx }, +})) + +vi.mock('@/lib/core/config/env', () => ({ + get env() { + return envRef + }, +})) + +import { validateSignupEmailMx } from '@/lib/messaging/email/validation.server' + +const mx = (...hosts: string[]) => + hosts.map((exchange, i) => ({ exchange, priority: (i + 1) * 10 })) + +describe('validateSignupEmailMx', () => { + beforeEach(() => { + vi.clearAllMocks() + envRef.BLOCKED_EMAIL_MX_HOSTS = undefined + }) + + it('blocks a domain whose MX backend is on the configured denylist', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = 'blocked-backend.example' + mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example')) + const result = await validateSignupEmailMx('user@rotated-domain.test') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('blocked_mx_backend') + }) + + it('matches the denylist as a case-insensitive substring of the MX exchange', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = 'Blocked-Backend.Example' + mockResolveMx.mockResolvedValue(mx('mx1.blocked-backend.example')) + const result = await validateSignupEmailMx('user@another-domain.test') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('blocked_mx_backend') + }) + + it('does not block any backend when the denylist is empty (no hardcoded defaults)', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = undefined + mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example')) + const result = await validateSignupEmailMx('user@rotated-domain.test') + expect(result.allowed).toBe(true) + }) + + it('allows a legitimate domain (gmail)', async () => { + mockResolveMx.mockResolvedValue( + mx('gmail-smtp-in.l.google.com', 'alt1.gmail-smtp-in.l.google.com') + ) + const result = await validateSignupEmailMx('real.person@gmail.com') + expect(result.allowed).toBe(true) + }) + + it('blocks a domain with no MX records (ENOTFOUND)', async () => { + mockResolveMx.mockRejectedValue(Object.assign(new Error('not found'), { code: 'ENOTFOUND' })) + const result = await validateSignupEmailMx('x@no-such-domain.invalid') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('no_mx') + }) + + it('blocks a domain that resolves to an empty MX set', async () => { + mockResolveMx.mockResolvedValue([]) + const result = await validateSignupEmailMx('x@empty.example') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('no_mx') + }) + + it('fails open on a transient DNS error (does not block legit users)', async () => { + mockResolveMx.mockRejectedValue(Object.assign(new Error('timeout'), { code: 'ETIMEOUT' })) + const result = await validateSignupEmailMx('user@some-real-domain.com') + expect(result.allowed).toBe(true) + }) + + it('allows when the email has no domain (defers to other validation)', async () => { + const result = await validateSignupEmailMx('not-an-email') + expect(result.allowed).toBe(true) + expect(mockResolveMx).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/messaging/email/validation.server.ts b/apps/sim/lib/messaging/email/validation.server.ts new file mode 100644 index 00000000000..2d1df5b3048 --- /dev/null +++ b/apps/sim/lib/messaging/email/validation.server.ts @@ -0,0 +1,98 @@ +import type { MxRecord } from 'dns' +import dns from 'dns/promises' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' + +const logger = createLogger('EmailValidationServer') + +const MX_LOOKUP_TIMEOUT_MS = 3000 + +/** + * MX-host substrings to block, supplied at runtime via `BLOCKED_EMAIL_MX_HOSTS`. + * + * Signup-spam botnets rotate throwaway domains rapidly but funnel them through a + * small number of shared catch-all mail providers, so the resolved MX host is a + * far more stable signal than the domain itself. Each entry is matched as a + * case-insensitive substring against the domain's resolved MX exchanges. No + * hosts are hardcoded — operators configure their own denylist out of band. + */ +function getBlockedMxHosts(): string[] { + return ( + env.BLOCKED_EMAIL_MX_HOSTS?.split(',') + .map((h) => h.trim().toLowerCase()) + .filter(Boolean) ?? [] + ) +} + +export interface SignupEmailCheck { + /** Whether the email may proceed to signup. */ + allowed: boolean + /** Machine-readable block reason, present only when `allowed` is false. */ + reason?: 'no_mx' | 'blocked_mx_backend' +} + +/** + * Server-side signup email validation backed by an MX lookup. + * + * Rejects domains that resolve to no mail server (`no_mx`) or to a denylisted + * catch-all backend (`blocked_mx_backend`). Designed to be fail-open: any DNS + * timeout or transient resolver error allows the signup through so legitimate + * users are never blocked by an infrastructure blip. Only a definitive + * "domain has no MX" answer (`ENOTFOUND` / `ENODATA`) blocks. + * + * Server-only — imports `dns/promises`. Never import from client code. Gated by the caller + * behind `isSignupMxValidationEnabled`; this function performs the check unconditionally. + */ +export async function validateSignupEmailMx(email: string): Promise { + const domain = email.split('@')[1]?.toLowerCase() + if (!domain) return { allowed: true } + + let records: MxRecord[] + let timeoutHandle: ReturnType | undefined + try { + records = await Promise.race([ + dns.resolveMx(domain), + new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error('mx_lookup_timeout')), + MX_LOOKUP_TIMEOUT_MS + ) + }), + ]) + } catch (error) { + const code = (error as NodeJS.ErrnoException).code + if (code === 'ENOTFOUND' || code === 'ENODATA') { + logger.info('Blocked signup: domain has no MX record', { domain }) + return { allowed: false, reason: 'no_mx' } + } + logger.warn('MX lookup failed; allowing signup (fail-open)', { + domain, + error: getErrorMessage(error), + }) + return { allowed: true } + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle) + } + + if (!records || records.length === 0) { + logger.info('Blocked signup: domain has no MX record', { domain }) + return { allowed: false, reason: 'no_mx' } + } + + const blocked = getBlockedMxHosts() + const match = records.find((record) => { + const exchange = record.exchange.toLowerCase() + return blocked.some((host) => exchange.includes(host)) + }) + + if (match) { + logger.info('Blocked signup: denylisted MX backend', { + domain, + exchange: match.exchange, + }) + return { allowed: false, reason: 'blocked_mx_backend' } + } + + return { allowed: true } +} diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 21d12053ebd..8ba80675de2 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -696,8 +696,10 @@ export const OAUTH_PROVIDERS: Record = { scopes: [ 'channels:read', 'channels:history', + 'channels:manage', 'groups:read', 'groups:history', + 'groups:write', 'chat:write', 'chat:write.public', 'im:write', diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 3630691217c..39320823081 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -8,6 +8,7 @@ export const PERMISSION_GROUP_MEMBER_CONSTRAINTS = { export const permissionGroupConfigSchema = z.object({ allowedIntegrations: z.array(z.string()).nullable().optional(), allowedModelProviders: z.array(z.string()).nullable().optional(), + deniedModels: z.array(z.string()).optional(), hideTraceSpans: z.boolean().optional(), hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), @@ -32,6 +33,11 @@ export const permissionGroupConfigSchema = z.object({ export interface PermissionGroupConfig { allowedIntegrations: string[] | null allowedModelProviders: string[] | null + /** + * Fully-qualified model IDs (e.g. `ollama/llama3`, `gpt-4o`) blocked for this + * group, checked after `allowedModelProviders`. Empty means nothing is blocked. + */ + deniedModels: string[] hideTraceSpans: boolean hideKnowledgeBaseTab: boolean hideTablesTab: boolean @@ -56,6 +62,7 @@ export interface PermissionGroupConfig { export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { allowedIntegrations: null, allowedModelProviders: null, + deniedModels: [], hideTraceSpans: false, hideKnowledgeBaseTab: false, hideTablesTab: false, @@ -87,6 +94,9 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf return { allowedIntegrations: Array.isArray(c.allowedIntegrations) ? c.allowedIntegrations : null, allowedModelProviders: Array.isArray(c.allowedModelProviders) ? c.allowedModelProviders : null, + deniedModels: Array.isArray(c.deniedModels) + ? c.deniedModels.filter((m): m is string => typeof m === 'string') + : [], hideTraceSpans: typeof c.hideTraceSpans === 'boolean' ? c.hideTraceSpans : false, hideKnowledgeBaseTab: typeof c.hideKnowledgeBaseTab === 'boolean' ? c.hideKnowledgeBaseTab : false, diff --git a/apps/sim/lib/table/dispatcher.ts b/apps/sim/lib/table/dispatcher.ts index 885df978bb0..441abda9330 100644 --- a/apps/sim/lib/table/dispatcher.ts +++ b/apps/sim/lib/table/dispatcher.ts @@ -3,7 +3,7 @@ import { tableRowExecutions, tableRunDispatches, userTableRows } from '@sim/db/s import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, asc, eq, gt, inArray, type SQL, sql } from 'drizzle-orm' +import { and, asc, eq, gt, inArray, isNotNull, ne, or, type SQL, sql } from 'drizzle-orm' import { getJobQueue } from '@/lib/core/async-jobs/config' import { writeWorkflowGroupState } from '@/lib/table/cell-write' import { appendTableEvent } from '@/lib/table/events' @@ -185,6 +185,15 @@ export async function insertDispatch(input: { * gutter Run/Stop button. All three statuses are user-cancellable, so the * gutter must surface Stop whenever any of them are present (else clicking * Play during the queued window would re-run an already-queued cell). + * + * Excludes orphan pre-stamps — `pending` rows with no `executionId` — which + * are dead placeholders left when a dispatcher loop wrote the stamp but no + * cell-task ever picked it up (lock contention, queue failure, crash). The + * cell already shows its prior value and `classifyEligibility` treats these as + * claimable, so counting them stuck the "X running" badge above zero forever + * even though nothing was running. Same `executionId == null` test used by + * {@link classifyEligibility} / {@link pickNextEligibleGroupForRow}. + * * Hits the `(table_id, status)` partial index on table_row_executions. */ export async function countRunningCells( tableId: string @@ -198,7 +207,10 @@ export async function countRunningCells( .where( and( eq(tableRowExecutions.tableId, tableId), - inArray(tableRowExecutions.status, ['queued', 'running', 'pending']) + inArray(tableRowExecutions.status, ['queued', 'running', 'pending']), + // Exclude orphan pre-stamps (`pending` + null executionId). De Morgan of + // NOT(pending AND null) — `status` is NOT NULL so `ne` is well-defined. + or(ne(tableRowExecutions.status, 'pending'), isNotNull(tableRowExecutions.executionId)) ) ) .groupBy(tableRowExecutions.rowId) diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 18f1fda418e..ae4bce8eb37 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -18,8 +18,10 @@ import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, count, eq, gt, gte, inArray, isNull, type SQL, sql } from 'drizzle-orm' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { generateRestoreName } from '@/lib/core/utils/restore-name' import type { DbOrTx } from '@/lib/db/types' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from './constants' import { buildFilterClause, buildSortClause } from './sql' import { fireTableTrigger } from './trigger' @@ -3935,18 +3937,26 @@ async function backfillGroupOutputsFromLogs(opts: { const logs = await db .select({ executionId: workflowExecutionLogs.executionId, + workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) .where(inArray(workflowExecutionLogs.executionId, executionIds)) const logByExecutionId = new Map() - for (const log of logs) { + // Heavy execution data may live in object storage; resolve pointers (bounded + // concurrency) so trace spans are available for table-column enrichment. + await mapWithConcurrency(logs, MATERIALIZE_CONCURRENCY, async (log) => { + const executionData = await materializeExecutionData( + log.executionData as Record | null, + { workspaceId: log.workspaceId, workflowId: log.workflowId, executionId: log.executionId } + ) logByExecutionId.set( log.executionId, - (log.executionData as { traceSpans?: BackfillTraceSpan[] }) ?? {} + (executionData as { traceSpans?: BackfillTraceSpan[] }) ?? {} ) - } + }) const updates: Array<{ rowId: string; data: RowData }> = [] for (const r of rowRecords) { diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index 2df30b8f9b4..bef5b8abbde 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -142,12 +142,14 @@ export interface TableSchema { /** * Table-level metadata stored alongside the table definition. UI state only - * (column widths, column order) — workflow-group concurrency is enforced at - * the trigger.dev queue layer, not via metadata. + * (column widths, column order, pinned columns) — workflow-group concurrency + * is enforced at the trigger.dev queue layer, not via metadata. */ export interface TableMetadata { columnWidths?: Record columnOrder?: string[] + /** Logical column names that are pinned to the left while scrolling horizontally. */ + pinnedColumns?: string[] } export interface TableDefinition { diff --git a/apps/sim/lib/workspaces/organization/types.ts b/apps/sim/lib/workspaces/organization/types.ts index e3f07c50c64..fd1e770eb43 100644 --- a/apps/sim/lib/workspaces/organization/types.ts +++ b/apps/sim/lib/workspaces/organization/types.ts @@ -71,7 +71,6 @@ interface MemberUsageData { isOverLimit: boolean role: string joinedAt: string - lastActive: string | null } interface OrganizationBillingData { diff --git a/apps/sim/providers/fireworks/index.test.ts b/apps/sim/providers/fireworks/index.test.ts new file mode 100644 index 00000000000..68fba04c736 --- /dev/null +++ b/apps/sim/providers/fireworks/index.test.ts @@ -0,0 +1,226 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockSupportsNativeStructuredOutputs, + mockPrepareToolsWithUsageControl, + mockExecuteTool, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockSupportsNativeStructuredOutputs: vi.fn(), + mockPrepareToolsWithUsageControl: vi.fn(), + mockExecuteTool: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn().mockReturnValue([]), + getProviderDefaultModel: vi.fn().mockReturnValue('llama-v3p1-70b-instruct'), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) + +vi.mock('@/providers/fireworks/utils', () => ({ + supportsNativeStructuredOutputs: mockSupportsNativeStructuredOutputs, + createReadableStreamFromOpenAIStream: vi.fn(() => ({}) as ReadableStream), + checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn().mockReturnValue({ input: 0, output: 0, total: 0 }), + generateSchemaInstructions: vi.fn(() => 'SCHEMA_INSTRUCTIONS'), + prepareToolExecution: vi.fn(() => ({ toolParams: { x: 1 }, executionParams: { x: 1 } })), + prepareToolsWithUsageControl: mockPrepareToolsWithUsageControl, + sumToolCosts: vi.fn().mockReturnValue(0), +})) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +import { fireworksProvider } from '@/providers/fireworks/index' +import { ProviderError } from '@/providers/types' + +const textResponse = (content: string) => ({ + choices: [{ message: { content, tool_calls: [] } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, +}) + +const toolCallResponse = () => ({ + choices: [ + { + message: { + content: null, + tool_calls: [ + { id: 'call_1', type: 'function', function: { name: 'my_tool', arguments: '{"x":1}' } }, + ], + }, + }, + ], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, +}) + +const toolDef = { + id: 'my_tool', + name: 'my_tool', + description: '', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, +} + +const callBody = (index: number) => mockCreate.mock.calls[index][0] +const lastCallBody = () => mockCreate.mock.calls.at(-1)?.[0] + +describe('fireworksProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSupportsNativeStructuredOutputs.mockResolvedValue(true) + mockPrepareToolsWithUsageControl.mockImplementation((tools) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + const baseRequest = { + model: 'fireworks/llama-v3p1-70b-instruct', + systemPrompt: 'You are helpful.', + messages: [{ role: 'user' as const, content: 'Hello' }], + apiKey: 'fw-test-key', + } + + it('throws when the API key is missing', async () => { + await expect( + fireworksProvider.executeRequest({ ...baseRequest, apiKey: undefined }) + ).rejects.toThrow('API key is required for Fireworks') + }) + + it('returns content and token usage for a simple request', async () => { + mockCreate.mockResolvedValueOnce(textResponse('hi there')) + + const result = await fireworksProvider.executeRequest(baseRequest) + + expect(result).toMatchObject({ + content: 'hi there', + model: 'llama-v3p1-70b-instruct', + tokens: { input: 10, output: 5, total: 15 }, + }) + }) + + it('wraps API errors in a ProviderError', async () => { + mockCreate.mockRejectedValueOnce(new Error('boom')) + + await expect(fireworksProvider.executeRequest(baseRequest)).rejects.toBeInstanceOf( + ProviderError + ) + }) + + it('streams directly when there are no tools', async () => { + mockCreate.mockResolvedValueOnce({}) + + const result = await fireworksProvider.executeRequest({ ...baseRequest, stream: true }) + + expect(lastCallBody()).toMatchObject({ stream: true, stream_options: { include_usage: true } }) + expect(result).toHaveProperty('stream') + expect(result).toHaveProperty('execution') + }) + + it('sends a json_schema response_format with no strict field', async () => { + mockCreate.mockResolvedValueOnce(textResponse('{}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' }, strict: true }, + }) + + expect(lastCallBody().response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'my_schema', schema: { type: 'object' } }, + }) + expect(lastCallBody().response_format.json_schema).not.toHaveProperty('strict') + }) + + it('falls back to json_object with prompt instructions when native is unsupported', async () => { + mockSupportsNativeStructuredOutputs.mockResolvedValue(false) + mockCreate.mockResolvedValueOnce(textResponse('{}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' } }, + }) + + expect(lastCallBody().response_format).toEqual({ type: 'json_object' }) + expect(lastCallBody().messages.at(-1)).toEqual({ + role: 'user', + content: 'SCHEMA_INSTRUCTIONS', + }) + }) + + it('defers response_format to a final call when tools are active', async () => { + mockCreate + .mockResolvedValueOnce(textResponse('intermediate')) + .mockResolvedValueOnce(textResponse('{"done":true}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' } }, + tools: [toolDef], + }) + + expect(mockCreate).toHaveBeenCalledTimes(2) + expect(callBody(0).response_format).toBeUndefined() + expect(callBody(0).tools).toBeDefined() + expect(callBody(1).response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'my_schema', schema: { type: 'object' } }, + }) + expect(callBody(1).tools).toBeUndefined() + }) + + it('runs the tool loop and threads tool results back into the conversation', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse()) + .mockResolvedValueOnce(textResponse('final answer')) + + const result = await fireworksProvider.executeRequest({ ...baseRequest, tools: [toolDef] }) + + expect(mockExecuteTool).toHaveBeenCalledWith('my_tool', { x: 1 }, expect.anything()) + expect(result).toMatchObject({ content: 'final answer' }) + expect((result as { toolCalls?: unknown[] }).toolCalls).toHaveLength(1) + + const followUpMessages = callBody(1).messages + expect(followUpMessages).toContainEqual( + expect.objectContaining({ role: 'assistant', tool_calls: expect.any(Array) }) + ) + expect(followUpMessages).toContainEqual( + expect.objectContaining({ role: 'tool', tool_call_id: 'call_1' }) + ) + }) + + it("forces tool_choice 'none' on the final streaming call after tools run", async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse()) + .mockResolvedValueOnce(textResponse('done')) + .mockResolvedValueOnce({}) + + await fireworksProvider.executeRequest({ ...baseRequest, stream: true, tools: [toolDef] }) + + expect(mockCreate).toHaveBeenCalledTimes(3) + expect(lastCallBody()).toMatchObject({ tool_choice: 'none', stream: true }) + }) +}) diff --git a/apps/sim/providers/fireworks/index.ts b/apps/sim/providers/fireworks/index.ts index 794ee3f0805..cdf355d3451 100644 --- a/apps/sim/providers/fireworks/index.ts +++ b/apps/sim/providers/fireworks/index.ts @@ -34,7 +34,7 @@ const logger = createLogger('FireworksProvider') /** * Applies structured output configuration to a payload based on model capabilities. - * Uses json_schema with strict mode for supported models, falls back to json_object with prompt instructions. + * Uses native json_schema for supported models, falls back to json_object with prompt instructions. */ async function applyResponseFormat( targetPayload: any, @@ -51,7 +51,6 @@ async function applyResponseFormat( json_schema: { name: responseFormat.name || 'response_schema', schema: responseFormat.schema || responseFormat, - strict: responseFormat.strict !== false, }, } return messages @@ -469,7 +468,7 @@ export const fireworksProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: [...currentMessages], - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } @@ -652,8 +651,3 @@ export const fireworksProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/litellm/index.test.ts b/apps/sim/providers/litellm/index.test.ts new file mode 100644 index 00000000000..5261f4e23d6 --- /dev/null +++ b/apps/sim/providers/litellm/index.test.ts @@ -0,0 +1,290 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCreate, mockExecuteTool } = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) + +vi.mock('@/lib/core/config/env', () => ({ + env: { LITELLM_BASE_URL: 'http://litellm.test', LITELLM_API_KEY: '' }, +})) + +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +vi.mock('@/providers/models', () => ({ + getProviderModels: () => [], + getProviderDefaultModel: () => '', +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: (messages: unknown) => messages, +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/litellm/utils', () => ({ + createReadableStreamFromLiteLLMStream: vi.fn( + () => new ReadableStream({ start: (c) => c.close() }) + ), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + sumToolCosts: vi.fn(() => 0), + prepareToolExecution: vi.fn((_tool, toolArgs) => ({ + toolParams: toolArgs, + executionParams: toolArgs, + })), + prepareToolsWithUsageControl: vi.fn((tools) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + })), + trackForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), + enforceStrictSchema: vi.fn((schema) => ({ ...schema, additionalProperties: false })), +})) + +import { litellmProvider } from '@/providers/litellm' +import { ProviderError } from '@/providers/types' + +interface ChatOptions { + content?: string | null + toolCalls?: Array<{ id: string; function: { name: string; arguments: string } }> + usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number } +} + +function chat({ content = null, toolCalls, usage }: ChatOptions = {}) { + return { + choices: [ + { + message: { content, tool_calls: toolCalls }, + finish_reason: toolCalls ? 'tool_calls' : 'stop', + }, + ], + usage: usage ?? { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + } +} + +function tool(name: string) { + return { id: name, name, description: 'd', parameters: {} } +} + +function run(request: Record) { + return litellmProvider.executeRequest!({ + model: 'litellm/llama-3', + messages: [{ role: 'user', content: 'Hi' }], + ...request, + } as never) as Promise +} + +const firstPayload = () => mockCreate.mock.calls[0][0] +const lastPayload = () => mockCreate.mock.calls.at(-1)![0] + +describe('litellmProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreate.mockResolvedValue(chat({ content: 'hello' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + it('assembles messages, strips the model prefix, and maps params', async () => { + const result = await run({ + systemPrompt: 'You are helpful.', + context: 'Some context', + temperature: 0.5, + maxTokens: 256, + }) + + const payload = firstPayload() + expect(payload.model).toBe('llama-3') + expect(payload.messages).toEqual([ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'Some context' }, + { role: 'user', content: 'Hi' }, + ]) + expect(payload.temperature).toBe(0.5) + expect(payload.max_completion_tokens).toBe(256) + expect(result.content).toBe('hello') + expect(result.tokens).toEqual({ input: 5, output: 3, total: 8 }) + }) + + it('forwards reasoning_effort only when set to a non-default value', async () => { + await run({ reasoningEffort: 'high' }) + expect(firstPayload().reasoning_effort).toBe('high') + + mockCreate.mockClear() + await run({ reasoningEffort: 'auto' }) + expect(firstPayload().reasoning_effort).toBeUndefined() + + mockCreate.mockClear() + await run({}) + expect(firstPayload().reasoning_effort).toBeUndefined() + }) + + it('sanitizes the schema for strict response_format and passes it through otherwise', async () => { + await run({ responseFormat: { name: 'r', schema: { type: 'object', properties: {} } } }) + let rf = firstPayload().response_format + expect(rf.type).toBe('json_schema') + expect(rf.json_schema.strict).toBe(true) + expect(rf.json_schema.schema.additionalProperties).toBe(false) + + mockCreate.mockClear() + await run({ + responseFormat: { name: 'r', schema: { type: 'object', properties: {} }, strict: false }, + }) + rf = firstPayload().response_format + expect(rf.json_schema.strict).toBe(false) + expect(rf.json_schema.schema.additionalProperties).toBeUndefined() + }) + + it('defers response_format past the tool loop and keeps tools on the final call', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{"q":1}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'mid' })) + .mockResolvedValueOnce(chat({ content: '{"answer":1}' })) + + const result = await run({ + tools: [tool('known')], + reasoningEffort: 'high', + responseFormat: { name: 'r', schema: { type: 'object', properties: {} } }, + }) + + expect(firstPayload().response_format).toBeUndefined() + expect(firstPayload().tools).toBeDefined() + + const final = lastPayload() + expect(final.response_format.type).toBe('json_schema') + expect(final.tools).toBeDefined() + expect(final.tool_choice).toBe('none') + expect(final.parallel_tool_calls).toBe(false) + expect(final.reasoning_effort).toBe('high') + expect(result.content).toBe('{"answer":1}') + }) + + it('defers response_format into the final streaming call while keeping tools', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'mid' })) + + const result = await run({ + stream: true, + tools: [tool('known')], + responseFormat: { name: 'r', schema: { type: 'object', properties: {} } }, + }) + + const final = lastPayload() + expect(final.stream).toBe(true) + expect(final.response_format.type).toBe('json_schema') + expect(final.tools).toBeDefined() + expect(final.tool_choice).toBe('none') + expect(final.parallel_tool_calls).toBe(false) + expect(result.execution.isStreaming).toBe(true) + }) + + it('threads assistant tool_calls and a named tool response, and reports toolCalls', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'done' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { temp: 72 } }) + + const result = await run({ tools: [tool('known')] }) + + const followupMessages = mockCreate.mock.calls[1][0].messages + expect(followupMessages).toContainEqual({ + role: 'assistant', + content: null, + tool_calls: [{ id: 'c1', type: 'function', function: { name: 'known', arguments: '{}' } }], + }) + expect(followupMessages).toContainEqual({ + role: 'tool', + tool_call_id: 'c1', + name: 'known', + content: JSON.stringify({ temp: 72 }), + }) + expect(result.toolCalls).toHaveLength(1) + expect(result.content).toBe('done') + }) + + it('emits a stub tool response for an unanswered tool_call_id', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'cX', function: { name: 'ghost', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'recovered' })) + + await run({ tools: [tool('known')] }) + + expect(mockExecuteTool).not.toHaveBeenCalled() + const followupMessages = mockCreate.mock.calls[1][0].messages + const toolMsg = followupMessages.find((m: any) => m.role === 'tool' && m.tool_call_id === 'cX') + expect(toolMsg).toBeDefined() + expect(toolMsg.content).toContain('not available') + }) + + it('executes a tool with empty arguments without failing', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'ping', arguments: '' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'pong' })) + + await run({ tools: [tool('ping')] }) + + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + const toolMsg = mockCreate.mock.calls[1][0].messages.find((m: any) => m.role === 'tool') + expect(toolMsg.content).not.toContain('"error":true') + }) + + it('stops the tool loop at MAX_TOOL_ITERATIONS', async () => { + mockCreate.mockResolvedValue( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + + await run({ tools: [tool('known')] }) + + expect(mockCreate).toHaveBeenCalledTimes(1 + 20) + expect(mockExecuteTool).toHaveBeenCalledTimes(20) + }) + + it('returns a streaming execution when streaming without active tools', async () => { + const result = await run({ stream: true }) + + expect(firstPayload().stream).toBe(true) + expect(firstPayload().stream_options).toEqual({ include_usage: true }) + expect(result.stream).toBeInstanceOf(ReadableStream) + expect(result.execution.isStreaming).toBe(true) + }) + + it('wraps API errors in a ProviderError using the error envelope message', async () => { + mockCreate.mockRejectedValue({ + error: { message: 'rate limited', type: 'rate_limit_error', code: '429' }, + }) + + await expect(run({})).rejects.toBeInstanceOf(ProviderError) + await expect(run({})).rejects.toThrow('rate limited') + }) +}) diff --git a/apps/sim/providers/litellm/index.ts b/apps/sim/providers/litellm/index.ts index 33e363f0509..53a5360d2c9 100644 --- a/apps/sim/providers/litellm/index.ts +++ b/apps/sim/providers/litellm/index.ts @@ -19,6 +19,7 @@ import type { import { ProviderError } from '@/providers/types' import { calculateCost, + enforceStrictSchema, prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, @@ -146,19 +147,27 @@ export const litellmProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens - if (request.responseFormat) { - payload.response_format = { - type: 'json_schema', - json_schema: { - name: request.responseFormat.name || 'response_schema', - schema: request.responseFormat.schema || request.responseFormat, - strict: request.responseFormat.strict !== false, - }, - } - - logger.info('Added JSON schema response format to LiteLLM request') + if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { + payload.reasoning_effort = request.reasoningEffort } + const isStrictResponseFormat = request.responseFormat + ? request.responseFormat.strict !== false + : false + + const responseFormatPayload = request.responseFormat + ? { + type: 'json_schema' as const, + json_schema: { + name: request.responseFormat.name || 'response_schema', + schema: isStrictResponseFormat + ? enforceStrictSchema(request.responseFormat.schema || request.responseFormat) + : request.responseFormat.schema || request.responseFormat, + strict: isStrictResponseFormat, + }, + } + : undefined + let preparedTools: ReturnType | null = null let hasActiveTools = false @@ -184,6 +193,12 @@ export const litellmProvider: ProviderConfig = { } } + const deferResponseFormat = !!responseFormatPayload && hasActiveTools + if (responseFormatPayload && !deferResponseFormat) { + payload.response_format = responseFormatPayload + logger.info('Added JSON schema response format to LiteLLM request') + } + const providerStartTime = Date.now() const providerStartTimeISO = new Date(providerStartTime).toISOString() @@ -271,6 +286,7 @@ export const litellmProvider: ProviderConfig = { endTime: new Date().toISOString(), duration: Date.now() - providerStartTime, }, + isStreaming: true, }, } as StreamingExecution @@ -374,7 +390,9 @@ export const litellmProvider: ProviderConfig = { const toolName = toolCall.function.name try { - const toolArgs = JSON.parse(toolCall.function.arguments) + const toolArgs = toolCall.function.arguments + ? JSON.parse(toolCall.function.arguments) + : {} const tool = request.tools?.find((t) => t.id === toolName) if (!tool) return null @@ -429,6 +447,8 @@ export const litellmProvider: ProviderConfig = { })), }) + const respondedToolCallIds = new Set() + for (const settledResult of executionResults) { if (settledResult.status === 'rejected' || !settledResult.value) continue @@ -469,8 +489,24 @@ export const litellmProvider: ProviderConfig = { currentMessages.push({ role: 'tool', tool_call_id: toolCall.id, + name: toolName, content: JSON.stringify(resultContent), }) + respondedToolCallIds.add(toolCall.id) + } + + for (const tc of toolCallsInResponse) { + if (respondedToolCallIds.has(tc.id)) continue + currentMessages.push({ + role: 'tool', + tool_call_id: tc.id, + name: tc.function.name, + content: JSON.stringify({ + error: true, + message: `Tool "${tc.function.name}" is not available`, + tool: tc.function.name, + }), + }) } const thisToolsTime = Date.now() - toolsStartTime @@ -551,10 +587,14 @@ export const litellmProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: currentMessages, - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } + if (deferResponseFormat && responseFormatPayload) { + streamingParams.response_format = responseFormatPayload + streamingParams.parallel_tool_calls = false + } const streamResponse = await litellm.chat.completions.create( streamingParams, request.abortSignal ? { signal: request.abortSignal } : undefined @@ -626,12 +666,59 @@ export const litellmProvider: ProviderConfig = { endTime: new Date().toISOString(), duration: Date.now() - providerStartTime, }, + isStreaming: true, }, } as StreamingExecution return streamingResult as StreamingExecution } + if (deferResponseFormat && responseFormatPayload) { + logger.info('Applying deferred JSON schema response format after tool processing') + + const finalFormatStartTime = Date.now() + const finalPayload: any = { + ...payload, + messages: currentMessages, + response_format: responseFormatPayload, + tool_choice: 'none', + parallel_tool_calls: false, + } + + currentResponse = await litellm.chat.completions.create( + finalPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const finalFormatEndTime = Date.now() + timeSegments.push({ + type: 'model', + name: request.model, + startTime: finalFormatStartTime, + endTime: finalFormatEndTime, + duration: finalFormatEndTime - finalFormatStartTime, + }) + modelTime += finalFormatEndTime - finalFormatStartTime + + const formattedContent = currentResponse.choices[0]?.message?.content + if (formattedContent) { + content = formattedContent.replace(/```json\n?|\n?```/g, '').trim() + } + + if (currentResponse.usage) { + tokens.input += currentResponse.usage.prompt_tokens || 0 + tokens.output += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + enrichLastModelSegmentFromChatCompletions( + timeSegments, + currentResponse, + currentResponse.choices[0]?.message?.tool_calls, + { model: request.model, provider: 'litellm' } + ) + } + const providerEndTime = Date.now() const providerEndTimeISO = new Date(providerEndTime).toISOString() const totalDuration = providerEndTime - providerStartTime @@ -660,7 +747,7 @@ export const litellmProvider: ProviderConfig = { let errorMessage = toError(error).message let errorType: string | undefined - let errorCode: number | undefined + let errorCode: string | number | undefined if (error && typeof error === 'object' && 'error' in error) { const litellmError = error.error as any diff --git a/apps/sim/providers/ollama/index.test.ts b/apps/sim/providers/ollama/index.test.ts new file mode 100644 index 00000000000..a6b91e9e8f5 --- /dev/null +++ b/apps/sim/providers/ollama/index.test.ts @@ -0,0 +1,351 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +type StreamUsage = { prompt_tokens: number; completion_tokens: number; total_tokens: number } + +const { mockCreate, mockExecuteTool, streamOnComplete, MockAPIError } = vi.hoisted(() => { + class MockAPIError extends Error { + status?: number + code?: string | null + type?: string + constructor(message: string, opts: { status?: number; code?: string; type?: string } = {}) { + super(message) + this.name = 'APIError' + this.status = opts.status + this.code = opts.code + this.type = opts.type + } + } + return { + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + streamOnComplete: { + current: undefined as undefined | ((content: string, usage: StreamUsage) => void), + }, + MockAPIError, + } +}) + +vi.mock('openai', () => { + const OpenAI = vi.fn(() => ({ chat: { completions: { create: mockCreate } } })) + ;(OpenAI as unknown as { APIError: typeof MockAPIError }).APIError = MockAPIError + return { default: OpenAI } +}) + +vi.mock('@/lib/core/utils/urls', () => ({ getOllamaUrl: () => 'http://localhost:11434' })) +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: (messages: unknown) => messages, +})) +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) +vi.mock('@/providers/ollama/utils', () => ({ + createReadableStreamFromOllamaStream: ( + _stream: unknown, + onComplete: (content: string, usage: StreamUsage) => void + ) => { + streamOnComplete.current = onComplete + return 'OLLAMA_STREAM' + }, +})) +vi.mock('@/providers/utils', () => ({ + calculateCost: () => ({ input: 0, output: 0, total: 0, pricing: null }), + prepareToolExecution: (_tool: unknown, args: Record) => ({ + toolParams: args, + executionParams: args, + }), + sumToolCosts: () => 0, +})) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +import { ollamaProvider } from '@/providers/ollama' +import type { ProviderRequest, ProviderResponse, ProviderToolConfig } from '@/providers/types' + +interface StreamingResult { + stream: string + execution: { + output: { + content: string + tokens: { input: number; output: number; total: number } + toolCalls?: { list: unknown[]; count: number } + } + } +} + +type ToolCallChunk = { id: string; type: 'function'; function: { name: string; arguments: string } } + +function completion( + opts: { content?: string | null; toolCalls?: ToolCallChunk[]; usage?: StreamUsage } = {} +) { + return { + choices: [{ message: { content: opts.content ?? null, tool_calls: opts.toolCalls } }], + usage: opts.usage ?? { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + } +} + +function makeTool(id: string, usageControl?: 'auto' | 'force' | 'none'): ProviderToolConfig { + return { + id, + name: id, + description: `${id} tool`, + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + ...(usageControl ? { usageControl } : {}), + } +} + +const baseRequest: ProviderRequest = { + model: 'llama3.2', + messages: [{ role: 'user', content: 'hi' }], +} + +describe('ollamaProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + streamOnComplete.current = undefined + mockCreate.mockResolvedValue(completion({ content: 'hello' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + it('assembles system, context, then history in order and forwards params', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + systemPrompt: 'be nice', + context: 'ctx', + temperature: 0.5, + maxTokens: 128, + })) as ProviderResponse + + expect(result).toMatchObject({ content: 'hello', model: 'llama3.2' }) + const payload = mockCreate.mock.calls[0][0] + expect(payload.messages).toEqual([ + { role: 'system', content: 'be nice' }, + { role: 'user', content: 'ctx' }, + { role: 'user', content: 'hi' }, + ]) + expect(payload.model).toBe('llama3.2') + expect(payload.temperature).toBe(0.5) + expect(payload.max_tokens).toBe(128) + }) + + it('returns content verbatim (keeps ```json fences) when no responseFormat', async () => { + const fenced = '```json\n{"a":1}\n```' + mockCreate.mockResolvedValue(completion({ content: fenced })) + const result = (await ollamaProvider.executeRequest(baseRequest)) as ProviderResponse + expect(result.content).toBe(fenced) + }) + + it('strips ```json fences and sends a json_schema response_format when requested', async () => { + mockCreate.mockResolvedValue(completion({ content: '```json\n{"a":1}\n```' })) + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'r', schema: { type: 'object' }, strict: true }, + })) as ProviderResponse + expect(result.content).toBe('{"a":1}') + expect(mockCreate.mock.calls[0][0].response_format).toMatchObject({ + type: 'json_schema', + json_schema: { name: 'r', schema: { type: 'object' }, strict: true }, + }) + }) + + it('runs the tool loop: parses string args, feeds results back, then terminates', async () => { + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{"x":1}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'done' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('mytool')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledWith('mytool', { x: 1 }, expect.anything()) + expect(mockCreate).toHaveBeenCalledTimes(2) + expect(result.content).toBe('done') + expect(result.toolCalls).toEqual([ + expect.objectContaining({ name: 'mytool', success: true, arguments: { x: 1 } }), + ]) + expect(result.toolResults).toEqual([{ ok: true }]) + + const followUp = mockCreate.mock.calls[1][0].messages + expect(followUp).toContainEqual( + expect.objectContaining({ + role: 'assistant', + content: null, + tool_calls: [ + expect.objectContaining({ + id: 'call_1', + function: { name: 'mytool', arguments: '{"x":1}' }, + }), + ], + }) + ) + expect(followUp).toContainEqual({ + role: 'tool', + tool_call_id: 'call_1', + content: JSON.stringify({ ok: true }), + }) + }) + + it('records a failed tool result without aborting the loop', async () => { + mockExecuteTool.mockResolvedValue({ success: false, error: 'boom' }) + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'recovered' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('mytool')], + })) as ProviderResponse + + expect(result.content).toBe('recovered') + expect(result.toolCalls?.[0]).toMatchObject({ name: 'mytool', success: false }) + const toolMsg = mockCreate.mock.calls[1][0].messages.find( + (m: { role: string }) => m.role === 'tool' + ) + expect(JSON.parse(toolMsg.content)).toMatchObject({ error: true, message: 'boom' }) + }) + + it('executes parallel tool calls from a single response', async () => { + mockExecuteTool + .mockResolvedValueOnce({ success: true, output: { from: 'a' } }) + .mockResolvedValueOnce({ success: true, output: { from: 'b' } }) + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_a', type: 'function', function: { name: 'a', arguments: '{}' } }, + { id: 'call_b', type: 'function', function: { name: 'b', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'summary' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('a'), makeTool('b')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledTimes(2) + expect(result.toolCalls?.map((c) => c.name)).toEqual(['a', 'b']) + const toolMsgs = mockCreate.mock.calls[1][0].messages.filter( + (m: { role: string }) => m.role === 'tool' + ) + expect(toolMsgs.map((m: { tool_call_id: string }) => m.tool_call_id)).toEqual([ + 'call_a', + 'call_b', + ]) + }) + + it('filters out tools with usageControl "none"', async () => { + await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('keep'), makeTool('drop', 'none')], + }) + const sent = mockCreate.mock.calls[0][0].tools + expect(sent.map((t: { function: { name: string } }) => t.function.name)).toEqual(['keep']) + }) + + it('never forces tools (Ollama ignores tool_choice) and keeps "auto"', async () => { + await ollamaProvider.executeRequest({ ...baseRequest, tools: [makeTool('forced', 'force')] }) + const payload = mockCreate.mock.calls[0][0] + expect(payload.tool_choice).toBe('auto') + expect(payload.tools.map((t: { function: { name: string } }) => t.function.name)).toEqual([ + 'forced', + ]) + }) + + it('surfaces an OpenAI APIError message through ProviderError', async () => { + mockCreate.mockRejectedValue( + new MockAPIError('model not found', { + status: 404, + code: 'not_found', + type: 'invalid_request_error', + }) + ) + await expect(ollamaProvider.executeRequest(baseRequest)).rejects.toThrow('model not found') + }) + + it('streams content and usage when no tools are used', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + })) as unknown as StreamingResult + + expect(result.stream).toBe('OLLAMA_STREAM') + expect(mockCreate.mock.calls[0][0].stream_options).toEqual({ include_usage: true }) + + streamOnComplete.current?.('streamed text', { + prompt_tokens: 4, + completion_tokens: 6, + total_tokens: 10, + }) + expect(result.execution.output.content).toBe('streamed text') + expect(result.execution.output.tokens).toMatchObject({ input: 4, output: 6, total: 10 }) + }) + + it('strips ```json fences from streamed content when responseFormat is set', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + responseFormat: { name: 'r', schema: { type: 'object' }, strict: true }, + })) as unknown as StreamingResult + + streamOnComplete.current?.('```json\n{"a":1}\n```', { + prompt_tokens: 1, + completion_tokens: 2, + total_tokens: 3, + }) + expect(result.execution.output.content).toBe('{"a":1}') + }) + + it('streams the final response after a tool loop, carrying tool calls', async () => { + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'intermediate' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + tools: [makeTool('mytool')], + })) as unknown as StreamingResult + + expect(result.stream).toBe('OLLAMA_STREAM') + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + + const finalCall = mockCreate.mock.calls[2][0] + expect(finalCall.tools).toBeUndefined() + expect(finalCall.tool_choice).toBeUndefined() + + streamOnComplete.current?.('final answer', { + prompt_tokens: 2, + completion_tokens: 4, + total_tokens: 6, + }) + expect(result.execution.output.content).toBe('final answer') + expect(result.execution.output.toolCalls).toMatchObject({ count: 1 }) + }) +}) diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index 52332aecdb2..bfe7cff6134 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { getErrorMessage, toError } from '@sim/utils/errors' +import { getErrorMessage } from '@sim/utils/errors' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' import { getOllamaUrl } from '@/lib/core/utils/urls' @@ -10,6 +10,7 @@ import type { ModelsObject } from '@/providers/ollama/types' import { createReadableStreamFromOllamaStream } from '@/providers/ollama/utils' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' import type { + Message, ProviderConfig, ProviderRequest, ProviderResponse, @@ -73,7 +74,7 @@ export const ollamaProvider: ProviderConfig = { baseURL: `${OLLAMA_HOST}/v1`, }) - const allMessages = [] + const allMessages: Message[] = [] if (request.systemPrompt) { allMessages.push({ @@ -92,7 +93,7 @@ export const ollamaProvider: ProviderConfig = { if (request.messages) { allMessages.push(...request.messages) } - const formattedMessages = formatMessagesForProvider(allMessages, 'ollama') + const formattedMessages = formatMessagesForProvider(allMessages, 'ollama') as Message[] const tools = request.tools?.length ? request.tools.map((tool) => ({ @@ -180,7 +181,7 @@ export const ollamaProvider: ProviderConfig = { stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => { streamingResult.execution.output.content = content - if (content) { + if (content && request.responseFormat) { streamingResult.execution.output.content = content .replace(/```json\n?|\n?```/g, '') .trim() @@ -264,7 +265,7 @@ export const ollamaProvider: ProviderConfig = { let content = currentResponse.choices[0]?.message?.content || '' - if (content) { + if (content && request.responseFormat) { content = content.replace(/```json\n?|\n?```/g, '') content = content.trim() } @@ -295,6 +296,9 @@ export const ollamaProvider: ProviderConfig = { while (iterationCount < MAX_TOOL_ITERATIONS) { if (currentResponse.choices[0]?.message?.content) { content = currentResponse.choices[0].message.content + if (request.responseFormat) { + content = content.replace(/```json\n?|\n?```/g, '').trim() + } } const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls @@ -450,8 +454,9 @@ export const ollamaProvider: ProviderConfig = { if (currentResponse.choices[0]?.message?.content) { content = currentResponse.choices[0].message.content - content = content.replace(/```json\n?|\n?```/g, '') - content = content.trim() + if (request.responseFormat) { + content = content.replace(/```json\n?|\n?```/g, '').trim() + } } if (currentResponse.usage) { @@ -477,10 +482,11 @@ export const ollamaProvider: ProviderConfig = { const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) + const { tools: _tools, tool_choice: _toolChoice, ...streamPayload } = payload + const streamingParams: ChatCompletionCreateParamsStreaming = { - ...payload, + ...streamPayload, messages: currentMessages, - tool_choice: 'auto', stream: true, stream_options: { include_usage: true }, } @@ -493,7 +499,7 @@ export const ollamaProvider: ProviderConfig = { stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => { streamingResult.execution.output.content = content - if (content) { + if (content && request.responseFormat) { streamingResult.execution.output.content = content .replace(/```json\n?|\n?```/g, '') .trim() @@ -589,12 +595,27 @@ export const ollamaProvider: ProviderConfig = { const providerEndTimeISO = new Date(providerEndTime).toISOString() const totalDuration = providerEndTime - providerStartTime + let errorMessage = getErrorMessage(error, 'Unknown error') + let errorType: string | undefined + let errorCode: string | undefined + let status: number | undefined + + if (error instanceof OpenAI.APIError) { + errorMessage = error.message + errorType = error.type + errorCode = error.code ?? undefined + status = error.status + } + logger.error('Error in Ollama request:', { - error, + error: errorMessage, + errorType, + errorCode, + status, duration: totalDuration, }) - throw new ProviderError(toError(error).message, { + throw new ProviderError(errorMessage, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, @@ -602,8 +623,3 @@ export const ollamaProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 6946f0c0fa3..6f19cef1562 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -8,6 +8,7 @@ import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/ import { ProviderError } from '@/providers/types' import { calculateCost, + enforceStrictSchema, prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, @@ -31,60 +32,6 @@ import { type PreparedTools = ReturnType type ToolChoice = PreparedTools['toolChoice'] -/** - * Recursively enforces OpenAI strict mode requirements on a JSON schema. - * - Sets additionalProperties: false on all object types. - * - Ensures required includes ALL property keys. - */ -function enforceStrictSchema(schema: Record): Record { - if (!schema || typeof schema !== 'object') return schema - - const result = { ...schema } - - // If this is an object type, enforce strict requirements - if (result.type === 'object') { - result.additionalProperties = false - - // Recursively process properties and ensure required includes all keys - if (result.properties && typeof result.properties === 'object') { - const propKeys = Object.keys(result.properties as Record) - result.required = propKeys // Strict mode requires ALL properties - result.properties = Object.fromEntries( - Object.entries(result.properties as Record).map(([key, value]) => [ - key, - enforceStrictSchema(value as Record), - ]) - ) - } - } - - // Handle array items - if (result.type === 'array' && result.items) { - result.items = enforceStrictSchema(result.items as Record) - } - - // Handle anyOf, oneOf, allOf - for (const keyword of ['anyOf', 'oneOf', 'allOf']) { - if (Array.isArray(result[keyword])) { - result[keyword] = (result[keyword] as Record[]).map(enforceStrictSchema) - } - } - - // Handle $defs / definitions - for (const defKey of ['$defs', 'definitions']) { - if (result[defKey] && typeof result[defKey] === 'object') { - result[defKey] = Object.fromEntries( - Object.entries(result[defKey] as Record).map(([key, value]) => [ - key, - enforceStrictSchema(value as Record), - ]) - ) - } - } - - return result -} - export interface ResponsesProviderConfig { providerId: string providerLabel: string diff --git a/apps/sim/providers/openrouter/index.test.ts b/apps/sim/providers/openrouter/index.test.ts new file mode 100644 index 00000000000..88339fe4a93 --- /dev/null +++ b/apps/sim/providers/openrouter/index.test.ts @@ -0,0 +1,345 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockExecuteTool, + mockSupportsNative, + mockPrepareTools, + mockCheckForced, + mockCreateStream, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + mockSupportsNative: vi.fn(), + mockPrepareTools: vi.fn((tools: unknown) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + })), + mockCheckForced: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), + mockCreateStream: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 10 })) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn().mockReturnValue([]), + getProviderDefaultModel: vi.fn().mockReturnValue(''), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages: unknown) => messages), +})) + +vi.mock('@/providers/openrouter/utils', () => ({ + supportsNativeStructuredOutputs: mockSupportsNative, + createReadableStreamFromOpenAIStream: mockCreateStream, + checkForForcedToolUsage: mockCheckForced, +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolsWithUsageControl: mockPrepareTools, + prepareToolExecution: vi.fn((_tool: unknown, toolArgs: Record) => ({ + toolParams: toolArgs, + executionParams: toolArgs, + })), + sumToolCosts: vi.fn(() => 0), + generateSchemaInstructions: vi.fn(() => 'SCHEMA_INSTRUCTIONS'), +})) + +import { openRouterProvider } from '@/providers/openrouter/index' +import type { ProviderRequest, ProviderResponse, ProviderToolConfig } from '@/providers/types' + +interface Usage { + prompt_tokens: number + completion_tokens: number + total_tokens: number +} + +function textResponse( + content: string, + usage: Usage = { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } +) { + return { + choices: [{ message: { content, tool_calls: undefined }, finish_reason: 'stop' }], + usage, + } +} + +function toolCallResponse(name: string, args: Record, id = 'call_1') { + return { + choices: [ + { + message: { + content: null, + tool_calls: [ + { id, type: 'function', function: { name, arguments: JSON.stringify(args) } }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, + } +} + +function tool(id: string): ProviderToolConfig { + return { + id, + name: id, + description: 'test tool', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + } +} + +const baseRequest: ProviderRequest = { + apiKey: 'sk-or-test', + model: 'openrouter/anthropic/claude-3.5-sonnet', + systemPrompt: 'You are helpful.', + messages: [{ role: 'user', content: 'Hello' }], +} + +describe('openRouterProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreate.mockReset() + mockExecuteTool.mockReset() + mockSupportsNative.mockResolvedValue(false) + }) + + it('requires an API key', async () => { + await expect( + openRouterProvider.executeRequest({ model: 'openrouter/x', messages: [] }) + ).rejects.toThrow('API key is required for OpenRouter') + }) + + it('strips the openrouter/ prefix and returns content + tokens', async () => { + mockCreate.mockResolvedValueOnce(textResponse('Hi there')) + + const res = (await openRouterProvider.executeRequest(baseRequest)) as ProviderResponse + + expect(res.content).toBe('Hi there') + expect(res.model).toBe('anthropic/claude-3.5-sonnet') + expect(res.tokens).toEqual({ input: 10, output: 5, total: 15 }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.model).toBe('anthropic/claude-3.5-sonnet') + expect(payload.messages[0]).toEqual({ role: 'system', content: 'You are helpful.' }) + expect(payload.messages.at(-1)).toEqual({ role: 'user', content: 'Hello' }) + }) + + it('inserts context as a user message between system and history', async () => { + mockCreate.mockResolvedValueOnce(textResponse('ok')) + + await openRouterProvider.executeRequest({ ...baseRequest, context: 'CTX' }) + + const { messages } = mockCreate.mock.calls[0][0] + expect(messages[0]).toEqual({ role: 'system', content: 'You are helpful.' }) + expect(messages[1]).toEqual({ role: 'user', content: 'CTX' }) + expect(messages[2]).toEqual({ role: 'user', content: 'Hello' }) + }) + + it('forwards maxTokens as max_tokens and temperature', async () => { + mockCreate.mockResolvedValueOnce(textResponse('ok')) + + await openRouterProvider.executeRequest({ ...baseRequest, maxTokens: 256, temperature: 0.4 }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.max_tokens).toBe(256) + expect(payload.temperature).toBe(0.4) + }) + + it('runs the tool loop: executes the tool, echoes tool_calls, returns the tool result, sums tokens', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse('get_weather', { city: 'SF' })) + .mockResolvedValueOnce( + textResponse('It is sunny', { prompt_tokens: 20, completion_tokens: 6, total_tokens: 26 }) + ) + mockExecuteTool.mockResolvedValueOnce({ success: true, output: { temp: 70 } }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledWith('get_weather', { city: 'SF' }, expect.anything()) + expect(res.content).toBe('It is sunny') + expect(res.toolCalls?.[0]).toMatchObject({ + name: 'get_weather', + result: { temp: 70 }, + success: true, + }) + expect(res.toolResults).toEqual([{ temp: 70 }]) + expect(res.tokens).toEqual({ input: 28, output: 10, total: 38 }) + + const secondMessages = mockCreate.mock.calls[1][0].messages + const assistant = secondMessages.find((m: { role: string }) => m.role === 'assistant') + expect(assistant).toMatchObject({ + content: null, + tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'get_weather' } }], + }) + const toolMsg = secondMessages.find((m: { role: string }) => m.role === 'tool') + expect(toolMsg).toEqual({ + role: 'tool', + tool_call_id: 'call_1', + content: JSON.stringify({ temp: 70 }), + }) + }) + + it('reports a failed tool result as an error payload to the model', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse('get_weather', { city: 'SF' })) + .mockResolvedValueOnce(textResponse('done')) + mockExecuteTool.mockResolvedValueOnce({ success: false, output: undefined, error: 'boom' }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + })) as ProviderResponse + + expect(res.toolResults).toBeUndefined() + expect(res.toolCalls?.[0]).toMatchObject({ success: false }) + const toolMsg = mockCreate.mock.calls[1][0].messages.find( + (m: { role: string }) => m.role === 'tool' + ) + expect(JSON.parse(toolMsg.content)).toEqual({ + error: true, + message: 'boom', + tool: 'get_weather', + }) + }) + + it('applies native structured outputs (json_schema + require_parameters) when no tools are active', async () => { + mockSupportsNative.mockResolvedValue(true) + mockCreate.mockResolvedValueOnce(textResponse('{"x":1}')) + + await openRouterProvider.executeRequest({ + ...baseRequest, + responseFormat: { + name: 'out', + schema: { type: 'object', properties: { x: { type: 'number' } } }, + strict: true, + }, + }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.response_format).toMatchObject({ + type: 'json_schema', + json_schema: { name: 'out', strict: true }, + }) + expect(payload.provider).toMatchObject({ require_parameters: true }) + }) + + it('falls back to json_object + prompt instructions when native structured outputs are unsupported', async () => { + mockSupportsNative.mockResolvedValue(false) + mockCreate.mockResolvedValueOnce(textResponse('{"x":1}')) + + await openRouterProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'out', schema: { type: 'object' } }, + }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.response_format).toEqual({ type: 'json_object' }) + expect(payload.messages.at(-1)).toEqual({ role: 'user', content: 'SCHEMA_INSTRUCTIONS' }) + }) + + it('defers response_format until after the tool loop when tools are active', async () => { + mockSupportsNative.mockResolvedValue(true) + mockCreate + .mockResolvedValueOnce(textResponse('interim')) + .mockResolvedValueOnce(textResponse('{"x":1}')) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + })) as ProviderResponse + + const toolCall = mockCreate.mock.calls[0][0] + expect(toolCall.tools).toBeDefined() + expect(toolCall.response_format).toBeUndefined() + + const finalCall = mockCreate.mock.calls[1][0] + expect(finalCall.response_format).toMatchObject({ type: 'json_schema' }) + expect(finalCall.tools).toBeUndefined() + expect(finalCall.tool_choice).toBeUndefined() + expect(res.content).toBe('{"x":1}') + }) + + it('forces the next tool after a forced tool is used', async () => { + mockPrepareTools.mockReturnValueOnce({ + tools: [tool('a')], + toolChoice: { type: 'function', function: { name: 'a' } }, + forcedTools: ['a', 'b'], + hasFilteredTools: false, + }) + mockCheckForced.mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['a'] }) + mockCreate + .mockResolvedValueOnce(toolCallResponse('a', {})) + .mockResolvedValueOnce(textResponse('done')) + mockExecuteTool.mockResolvedValueOnce({ success: true, output: {} }) + + await openRouterProvider.executeRequest({ ...baseRequest, tools: [tool('a'), tool('b')] }) + + expect(mockCreate.mock.calls[0][0].tool_choice).toEqual({ + type: 'function', + function: { name: 'a' }, + }) + expect(mockCreate.mock.calls[1][0].tool_choice).toEqual({ + type: 'function', + function: { name: 'b' }, + }) + }) + + it('streams directly when there are no tools and sends usage opt-in', async () => { + mockCreate.mockResolvedValueOnce({}) + + const res = await openRouterProvider.executeRequest({ ...baseRequest, stream: true }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.stream).toBe(true) + expect(payload.stream_options).toEqual({ include_usage: true }) + expect(mockCreateStream).toHaveBeenCalledTimes(1) + expect(res).toHaveProperty('stream') + expect(res).toHaveProperty('execution.output.model', 'anthropic/claude-3.5-sonnet') + }) + + it('stops the tool loop at MAX_TOOL_ITERATIONS', async () => { + mockCreate.mockResolvedValue(toolCallResponse('looping', {})) + mockExecuteTool.mockResolvedValue({ success: true, output: {} }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('looping')], + })) as ProviderResponse + + expect(mockCreate).toHaveBeenCalledTimes(11) + expect(mockExecuteTool).toHaveBeenCalledTimes(10) + expect(res.toolCalls?.length).toBe(10) + }) + + it('wraps SDK errors in a ProviderError', async () => { + mockCreate.mockRejectedValueOnce(new Error('rate limited')) + + await expect(openRouterProvider.executeRequest(baseRequest)).rejects.toThrow('rate limited') + }) +}) diff --git a/apps/sim/providers/openrouter/index.ts b/apps/sim/providers/openrouter/index.ts index d3d2535b43d..9bc180bdd11 100644 --- a/apps/sim/providers/openrouter/index.ts +++ b/apps/sim/providers/openrouter/index.ts @@ -376,8 +376,8 @@ export const openRouterProvider: ProviderConfig = { }) let resultContent: any - if (result.success) { - toolResults.push(result.output!) + if (result.success && result.output) { + toolResults.push(result.output) resultContent = result.output } else { resultContent = { @@ -653,8 +653,3 @@ export const openRouterProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/openrouter/utils.ts b/apps/sim/providers/openrouter/utils.ts index 8d8dade8279..51637f5148d 100644 --- a/apps/sim/providers/openrouter/utils.ts +++ b/apps/sim/providers/openrouter/utils.ts @@ -20,9 +20,6 @@ let modelCapabilitiesCache: Map | null = null let cacheTimestamp = 0 const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes -/** - * Fetches and caches OpenRouter model capabilities from their API. - */ async function fetchModelCapabilities(): Promise> { try { const response = await fetch('https://openrouter.ai/api/v1/models', { @@ -82,18 +79,11 @@ export async function getOpenRouterModelCapabilities( return modelCapabilitiesCache.get(normalizedId) ?? null } -/** - * Checks if a model supports native structured outputs (json_schema). - */ export async function supportsNativeStructuredOutputs(modelId: string): Promise { const capabilities = await getOpenRouterModelCapabilities(modelId) return capabilities?.supportsStructuredOutputs ?? false } -/** - * Creates a ReadableStream from an OpenRouter streaming response. - * Uses the shared OpenAI-compatible streaming utility. - */ export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, onComplete?: (content: string, usage: CompletionUsage) => void @@ -101,10 +91,6 @@ export function createReadableStreamFromOpenAIStream( return createOpenAICompatibleStream(openaiStream, 'OpenRouter', onComplete) } -/** - * Checks if a forced tool was used in an OpenRouter response. - * Uses the shared OpenAI-compatible forced tool usage helper. - */ export function checkForForcedToolUsage( response: any, toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 205fb307873..5cce6ce387e 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -3,7 +3,7 @@ import { getErrorMessage } from '@sim/utils/errors' import type OpenAI from 'openai' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { formatCreditCost } from '@/lib/billing/credits/conversion' import { env } from '@/lib/core/config/env' import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags' import { @@ -669,6 +669,59 @@ export function calculateCost( } } +/** + * Recursively enforces OpenAI strict-mode requirements on a JSON schema: + * - Sets `additionalProperties: false` on every object type. + * - Forces `required` to include ALL property keys. + * + * Required for any OpenAI-compatible backend that validates strict structured + * outputs (OpenAI, Azure OpenAI, and OpenAI routes behind proxies like LiteLLM), + * which reject schemas missing these constraints with an HTTP 400. + */ +export function enforceStrictSchema(schema: Record): Record { + if (!schema || typeof schema !== 'object') return schema + + const result = { ...schema } + + if (result.type === 'object') { + result.additionalProperties = false + + if (result.properties && typeof result.properties === 'object') { + const propKeys = Object.keys(result.properties as Record) + result.required = propKeys + result.properties = Object.fromEntries( + Object.entries(result.properties as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) + ) + } + } + + if (result.type === 'array' && result.items) { + result.items = enforceStrictSchema(result.items as Record) + } + + for (const keyword of ['anyOf', 'oneOf', 'allOf']) { + if (Array.isArray(result[keyword])) { + result[keyword] = (result[keyword] as Record[]).map(enforceStrictSchema) + } + } + + for (const defKey of ['$defs', 'definitions']) { + if (result[defKey] && typeof result[defKey] === 'object') { + result[defKey] = Object.fromEntries( + Object.entries(result[defKey] as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) + ) + } + } + + return result +} + /** * Sums the `cost.total` from each tool result returned during a provider tool loop. * Tool results may carry a `cost` object injected by `applyHostedKeyCostToResult`. @@ -700,11 +753,7 @@ export function getModelPricing(modelId: string): any { * @returns Formatted credit string (e.g. "200 credits", "<1 credit", "0 credits") */ export function formatCost(cost: number): string { - if (cost === undefined || cost === null) return '—' - const credits = dollarsToCredits(cost) - if (credits <= 0 && cost > 0) return '<1 credit' - if (credits <= 0) return '0 credits' - return `${credits.toLocaleString()} credits` + return formatCreditCost(cost) ?? '—' } /** diff --git a/apps/sim/providers/vllm/index.test.ts b/apps/sim/providers/vllm/index.test.ts new file mode 100644 index 00000000000..4477beeeda7 --- /dev/null +++ b/apps/sim/providers/vllm/index.test.ts @@ -0,0 +1,296 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockExecuteTool, + mockPrepareTools, + mockCheckForced, + mockCreateStream, + envState, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + mockPrepareTools: vi.fn(), + mockCheckForced: vi.fn(), + mockCreateStream: vi.fn(), + envState: { + VLLM_BASE_URL: 'http://localhost:8000', + VLLM_API_KEY: undefined as string | undefined, + }, +})) + +vi.mock('openai', () => ({ + default: vi.fn(() => ({ chat: { completions: { create: mockCreate } } })), +})) +vi.mock('@/lib/core/config/env', () => ({ env: envState })) +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => []), + getProviderDefaultModel: vi.fn(() => 'vllm/generic'), +})) +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })), + prepareToolsWithUsageControl: mockPrepareTools, + sumToolCosts: vi.fn(() => 0), +})) +vi.mock('@/providers/vllm/utils', () => ({ + checkForForcedToolUsage: mockCheckForced, + createReadableStreamFromVLLMStream: mockCreateStream, +})) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +import type { ProviderToolConfig } from '@/providers/types' +import { vllmProvider } from '@/providers/vllm/index' + +interface ToolCall { + id: string + type: 'function' + function: { name: string; arguments: string } +} + +function chatResponse(content: string | null, toolCalls?: ToolCall[]) { + return { + choices: [{ message: { content, tool_calls: toolCalls } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + } +} + +function makeTool(id: string): ProviderToolConfig { + return { + id, + name: id, + description: '', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + } +} + +const toolCall = (id: string, name: string, args = '{}'): ToolCall => ({ + id, + type: 'function', + function: { name, arguments: args }, +}) + +/** Payload passed to the Nth `chat.completions.create` call. */ +const createPayload = (callIndex: number) => mockCreate.mock.calls[callIndex][0] + +describe('vllmProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + envState.VLLM_BASE_URL = 'http://localhost:8000' + envState.VLLM_API_KEY = undefined + mockPrepareTools.mockReturnValue({ + tools: [{ type: 'function', function: { name: 'myTool' } }], + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + }) + mockCheckForced.mockReturnValue({ hasUsedForcedTool: false, usedForcedTools: [] }) + mockCreateStream.mockReturnValue(new ReadableStream({ start: (c) => c.close() })) + mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + }) + + it('builds a chat payload with the vllm/ prefix stripped and messages assembled in order', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('hello')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + systemPrompt: 'be helpful', + context: 'prior context', + messages: [{ role: 'user', content: 'hi' }], + temperature: 0.7, + maxTokens: 256, + }) + + const payload = createPayload(0) + expect(payload.model).toBe('llama-3') + expect(payload.temperature).toBe(0.7) + expect(payload.max_completion_tokens).toBe(256) + expect(payload.messages.map((m: { role: string }) => m.role)).toEqual([ + 'system', + 'user', + 'user', + ]) + expect(result.content).toBe('hello') + expect(result.tokens).toEqual({ input: 10, output: 5, total: 15 }) + }) + + it('sends response_format as json_schema with strict when a responseFormat is provided', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('{}')) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + + expect(createPayload(0).response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + }) + + it('strips markdown code fences from structured-output content', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('```json\n{"a":1}\n```')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + + expect(result.content).toBe('{"a":1}') + }) + + it('runs the tool loop: executes tools, appends assistant + tool messages, returns results', async () => { + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool', '{"x":1}')])) + .mockResolvedValueOnce(chatResponse('final answer')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'use a tool' }], + tools: [makeTool('myTool')], + }) + + expect(mockExecuteTool).toHaveBeenCalledWith('myTool', { x: 1 }, expect.anything()) + + const [assistantMessage, toolMessage] = createPayload(1).messages.slice(-2) + expect(assistantMessage).toMatchObject({ + role: 'assistant', + content: null, + tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'myTool' } }], + }) + expect(toolMessage).toMatchObject({ role: 'tool', tool_call_id: 'call_1' }) + expect(toolMessage).not.toHaveProperty('name') + + expect(result.content).toBe('final answer') + expect(result.toolCalls).toHaveLength(1) + expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: true }) + expect(result.toolResults).toHaveLength(1) + }) + + it('records a failed tool result without throwing', async () => { + mockExecuteTool.mockResolvedValueOnce({ success: false, error: 'tool blew up' }) + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')])) + .mockResolvedValueOnce(chatResponse('done')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('myTool')], + }) + + expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: false }) + const toolMessage = createPayload(1).messages.at(-1) + expect(JSON.parse(toolMessage.content)).toMatchObject({ error: true, tool: 'myTool' }) + }) + + it('surfaces a ProviderError when a follow-up model call fails mid-loop', async () => { + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')])) + .mockRejectedValueOnce(new Error('connection reset')) + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('myTool')], + }) + ).rejects.toThrow('connection reset') + + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + }) + + it('cycles forced tools: forces the next forced tool after the first is used', async () => { + mockPrepareTools.mockReturnValue({ + tools: [{ type: 'function', function: { name: 'toolA' } }], + toolChoice: { type: 'function', function: { name: 'toolA' } }, + forcedTools: ['toolA', 'toolB'], + hasFilteredTools: false, + }) + mockCheckForced + .mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA'] }) + .mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA', 'toolB'] }) + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('c1', 'toolA')])) + .mockResolvedValueOnce(chatResponse('done')) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('toolA'), makeTool('toolB')], + }) + + expect(createPayload(1).tool_choice).toEqual({ type: 'function', function: { name: 'toolB' } }) + }) + + it('streams directly when there are no tools, requesting usage in the stream', async () => { + mockCreate.mockResolvedValueOnce({}) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + stream: true, + }) + + expect(mockCreate).toHaveBeenCalledTimes(1) + const payload = createPayload(0) + expect(payload.stream).toBe(true) + expect(payload.stream_options).toEqual({ include_usage: true }) + expect('stream' in result && 'execution' in result).toBe(true) + }) + + it('uses tool_choice "none" on the final streaming call after tool processing', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('answer')).mockResolvedValueOnce({}) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + stream: true, + tools: [makeTool('myTool')], + }) + + const streamingPayload = createPayload(1) + expect(streamingPayload.stream).toBe(true) + expect(streamingPayload.tool_choice).toBe('none') + }) + + it('throws a ProviderError carrying the vLLM error message on API failure', async () => { + mockCreate.mockRejectedValueOnce({ + error: { message: 'bad request', type: 'invalid', code: 400 }, + }) + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + }) + ).rejects.toThrow('bad request') + }) + + it('throws when no base URL is configured', async () => { + envState.VLLM_BASE_URL = '' + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + }) + ).rejects.toThrow('VLLM_BASE_URL is required') + }) +}) diff --git a/apps/sim/providers/vllm/index.ts b/apps/sim/providers/vllm/index.ts index 2de3c695116..87610d9c43e 100644 --- a/apps/sim/providers/vllm/index.ts +++ b/apps/sim/providers/vllm/index.ts @@ -21,9 +21,8 @@ import { prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, - trackForcedToolUsage, } from '@/providers/utils' -import { createReadableStreamFromVLLMStream } from '@/providers/vllm/utils' +import { checkForForcedToolUsage, createReadableStreamFromVLLMStream } from '@/providers/vllm/utils' import { useProvidersStore } from '@/stores/providers' import { executeTool } from '@/tools' @@ -282,25 +281,7 @@ export const vllmProvider: ProviderConfig = { const forcedTools = preparedTools?.forcedTools || [] let usedForcedTools: string[] = [] - - const checkForForcedToolUsage = ( - response: any, - toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any } - ) => { - if (typeof toolChoice === 'object' && response.choices[0]?.message?.tool_calls) { - const toolCallsResponse = response.choices[0].message.tool_calls - const result = trackForcedToolUsage( - toolCallsResponse, - toolChoice, - logger, - 'vllm', - forcedTools, - usedForcedTools - ) - hasUsedForcedTool = result.hasUsedForcedTool - usedForcedTools = result.usedForcedTools - } - } + let hasUsedForcedTool = false let currentResponse = await vllm.chat.completions.create( payload, @@ -327,8 +308,6 @@ export const vllmProvider: ProviderConfig = { let modelTime = firstResponseTime let toolsTime = 0 - let hasUsedForcedTool = false - const timeSegments: TimeSegment[] = [ { type: 'model', @@ -339,7 +318,16 @@ export const vllmProvider: ProviderConfig = { }, ] - checkForForcedToolUsage(currentResponse, originalToolChoice) + if (originalToolChoice) { + const forcedResult = checkForForcedToolUsage( + currentResponse, + originalToolChoice, + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = forcedResult.hasUsedForcedTool + usedForcedTools = forcedResult.usedForcedTools + } while (iterationCount < MAX_TOOL_ITERATIONS) { if (currentResponse.choices[0]?.message?.content) { @@ -502,7 +490,16 @@ export const vllmProvider: ProviderConfig = { request.abortSignal ? { signal: request.abortSignal } : undefined ) - checkForForcedToolUsage(currentResponse, nextPayload.tool_choice) + if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') { + const forcedResult = checkForForcedToolUsage( + currentResponse, + nextPayload.tool_choice, + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = forcedResult.hasUsedForcedTool + usedForcedTools = forcedResult.usedForcedTools + } const nextModelEndTime = Date.now() const thisModelTime = nextModelEndTime - nextModelStartTime @@ -550,7 +547,7 @@ export const vllmProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: currentMessages, - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } @@ -685,8 +682,3 @@ export const vllmProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/vllm/utils.ts b/apps/sim/providers/vllm/utils.ts index 6f433291488..2b1db5bf553 100644 --- a/apps/sim/providers/vllm/utils.ts +++ b/apps/sim/providers/vllm/utils.ts @@ -1,6 +1,6 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' /** * Creates a ReadableStream from a vLLM streaming response. @@ -12,3 +12,16 @@ export function createReadableStreamFromVLLMStream( ): ReadableStream { return createOpenAICompatibleStream(vllmStream, 'vLLM', onComplete) } + +/** + * Checks if a forced tool was used in a vLLM response. + * Uses the shared OpenAI-compatible forced tool usage helper. + */ +export function checkForForcedToolUsage( + response: any, + toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, + forcedTools: string[], + usedForcedTools: string[] +): { hasUsedForcedTool: boolean; usedForcedTools: string[] } { + return checkForForcedToolUsageOpenAI(response, toolChoice, 'vLLM', forcedTools, usedForcedTools) +} diff --git a/apps/sim/scripts/backfill-trace-spans.ts b/apps/sim/scripts/backfill-trace-spans.ts new file mode 100644 index 00000000000..1fb3a9fd8f3 --- /dev/null +++ b/apps/sim/scripts/backfill-trace-spans.ts @@ -0,0 +1,188 @@ +#!/usr/bin/env bun + +/** + * One-shot, idempotent, resumable backfill that externalizes inline heavy + * `execution_data` (traceSpans, finalOutput, workflowInput, ...) into the + * execution-context large-value store, matching the completion path (cost-stripped + * spans, trace pointer + markers, owner/dependency + execution_log reference + * registration). Skips running rows and rows already carrying the pointer. + * + * Requires object storage to be configured; self-hosted deployments without it + * keep `execution_data` inline (reads resolve inline transparently) and can skip + * this script entirely. + * + * NOTE: the companion `cost_total` / `models_used` backfill is done in SQL by + * migration 0220 (batched, idempotent), so it runs for everyone — including + * self-hosted — and is intentionally NOT part of this script. + * + * Usage: + * DATABASE_URL=... bun apps/sim/scripts/backfill-trace-spans.ts [--max-batches=] + */ + +import { db } from '@sim/db' +import { workflowExecutionLogs } from '@sim/db/schema' +import { toError } from '@sim/utils/errors' +import { and, asc, eq, gt, sql } from 'drizzle-orm' +import { + collectLargeValueReferenceKeys, + replaceLargeValueReferenceKeysWithClient, +} from '@/lib/execution/payloads/large-value-metadata' +import { + externalizeExecutionData, + stripSpanCosts, + TRACE_STORE_REF_KEY, +} from '@/lib/logs/execution/trace-store' + +const TRACE_BATCH_SIZE = 100 + +/** + * Recursively counts trace spans (matching the completion path). Legacy rows + * predate the inline hasTraceSpans/traceSpanCount markers, so we derive them + * before externalizing — otherwise a post-expiry degraded read can't report + * "trace data expired (N spans)". + */ +function countTraceSpans(spans: unknown): number { + if (!Array.isArray(spans)) return 0 + return spans.reduce( + (count: number, span) => + count + 1 + countTraceSpans((span as { children?: unknown } | null)?.children), + 0 + ) +} + +interface Options { + maxBatches: number +} + +function parseArgs(argv: string[]): Options { + const maxBatchesArg = argv.find((a) => a.startsWith('--max-batches=')) + const maxBatches = maxBatchesArg + ? Number.parseInt(maxBatchesArg.slice('--max-batches='.length), 10) + : Number.POSITIVE_INFINITY + + if (Number.isNaN(maxBatches) || maxBatches <= 0) { + throw new Error('--max-batches must be a positive integer') + } + + return { maxBatches } +} + +/** Externalize inline heavy execution_data into the large-value store. */ +async function backfillTraceStorage( + maxBatches: number +): Promise<{ migrated: number; failed: number }> { + let migrated = 0 + let failed = 0 + // Keyset cursor by id: every row is visited at most once per run, so rows that + // can't be externalized (storage error, oversized) aren't re-selected into an + // infinite loop. A fresh re-run (cursor reset) retries any that failed. + let lastId = '' + + for (let batch = 0; batch < maxBatches; batch++) { + const rows = await db + .select({ + id: workflowExecutionLogs.id, + workspaceId: workflowExecutionLogs.workspaceId, + workflowId: workflowExecutionLogs.workflowId, + executionId: workflowExecutionLogs.executionId, + executionData: workflowExecutionLogs.executionData, + }) + .from(workflowExecutionLogs) + .where( + and( + sql`${workflowExecutionLogs.endedAt} IS NOT NULL`, + // Skip deleted-workflow rows: externalization requires a workflowId. + sql`${workflowExecutionLogs.workflowId} IS NOT NULL`, + sql`${workflowExecutionLogs.executionData} ? 'traceSpans'`, + sql`NOT (${workflowExecutionLogs.executionData} ? ${TRACE_STORE_REF_KEY})`, + lastId ? gt(workflowExecutionLogs.id, lastId) : undefined + ) + ) + .orderBy(asc(workflowExecutionLogs.id)) + .limit(TRACE_BATCH_SIZE) + + if (rows.length === 0) break + + for (const row of rows) { + try { + const executionData = (row.executionData ?? {}) as Record + // Derive the inline markers legacy rows lack so externalizeExecutionData + // carries them onto the slim row (they survive object expiry). + const traceSpanCount = countTraceSpans(executionData.traceSpans) + executionData.hasTraceSpans = traceSpanCount > 0 + executionData.traceSpanCount = traceSpanCount + stripSpanCosts(executionData.traceSpans) + // workspace_files.user_id (NOT NULL) needs the execution owner; legacy + // rows carry it under executionData.environment.userId. Rows without an + // owner can't be externalized — count them as failed and skip. + const environment = executionData.environment as { userId?: string } | undefined + const ownerUserId = environment?.userId + if (!ownerUserId) { + failed++ + continue + } + const slim = await externalizeExecutionData(executionData, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + userId: ownerUserId, + }) + + if (!(TRACE_STORE_REF_KEY in slim)) { + failed++ + continue + } + + await db.transaction(async (tx) => { + await tx + .update(workflowExecutionLogs) + .set({ executionData: slim }) + .where(eq(workflowExecutionLogs.id, row.id)) + + await replaceLargeValueReferenceKeysWithClient( + tx, + { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + source: 'execution_log', + }, + collectLargeValueReferenceKeys(slim) + ) + }) + + migrated++ + } catch (error) { + failed++ + console.error(` [trace] row ${row.id} failed: ${toError(error).message}`) + } + } + + // Advance the cursor past this batch so failed rows aren't re-selected. + lastId = rows[rows.length - 1].id + + console.log(` [trace] batch ${batch + 1}: migrated ${migrated}, failed ${failed}`) + } + + return { migrated, failed } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)) + const startedAt = Date.now() + + console.log('Backfilling trace storage (externalizing execution_data)…') + const { migrated, failed } = await backfillTraceStorage(options.maxBatches) + console.log(`Trace storage done: ${migrated} migrated, ${failed} skipped/failed.`) + + console.log(`Backfill complete in ${((Date.now() - startedAt) / 1000).toFixed(1)}s.`) +} + +main() + .catch((err) => { + console.error('Backfill failed:', err) + process.exit(1) + }) + .finally(() => { + process.exit(0) + }) diff --git a/apps/sim/stores/table/types.ts b/apps/sim/stores/table/types.ts index 13f9f999c43..68496d3cc81 100644 --- a/apps/sim/stores/table/types.ts +++ b/apps/sim/stores/table/types.ts @@ -44,6 +44,7 @@ export type TableUndoAction = cellData: Array<{ rowId: string; value: unknown }> previousOrder: string[] | null previousWidth: number | null + previousPinnedColumns: string[] | null } | { type: 'rename-column'; oldName: string; newName: string } | { diff --git a/apps/sim/tools/enrichment-hosting.test.ts b/apps/sim/tools/enrichment-hosting.test.ts new file mode 100644 index 00000000000..fcb33b31d03 --- /dev/null +++ b/apps/sim/tools/enrichment-hosting.test.ts @@ -0,0 +1,277 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { findEmailFromNameTool } from '@/tools/findymail/find_email_from_name' +import { findEmailsByDomainTool } from '@/tools/findymail/find_emails_by_domain' +import { findPhoneTool } from '@/tools/findymail/find_phone' +import { FINDYMAIL_CREDIT_USD } from '@/tools/findymail/hosting' +import { reverseEmailLookupTool } from '@/tools/findymail/reverse_email_lookup' +import { verifyEmailTool } from '@/tools/findymail/verify_email' +import { bulkEnrichPersonTool } from '@/tools/prospeo/bulk_enrich_person' +import { enrichCompanyTool } from '@/tools/prospeo/enrich_company' +import { enrichPersonTool } from '@/tools/prospeo/enrich_person' +import { PROSPEO_CREDIT_USD } from '@/tools/prospeo/hosting' +import { searchPersonTool } from '@/tools/prospeo/search_person' +import type { ToolConfig } from '@/tools/types' +import { wizaCompanyEnrichmentTool } from '@/tools/wiza/company_enrichment' +import { WIZA_CREDIT_USD } from '@/tools/wiza/hosting' +import { wizaIndividualRevealTool } from '@/tools/wiza/individual_reveal' +import { wizaProspectSearchTool } from '@/tools/wiza/prospect_search' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Findymail hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(findEmailFromNameTool.hosting?.envKeyPrefix).toBe('FINDYMAIL_API_KEY') + expect(findEmailFromNameTool.hosting?.byokProviderId).toBe('findymail') + }) + + it('charges one credit only when an email is found', () => { + expect(cost(findEmailFromNameTool, {}, { contact: { email: 'a@b.com' } }).cost).toBeCloseTo( + FINDYMAIL_CREDIT_USD + ) + expect(cost(findEmailFromNameTool, {}, { contact: null }).cost).toBe(0) + }) + + it('charges 10 credits for a found phone', () => { + expect(cost(findPhoneTool, {}, { phone: '+1555' }).cost).toBeCloseTo(10 * FINDYMAIL_CREDIT_USD) + expect(cost(findPhoneTool, {}, { phone: null }).cost).toBe(0) + }) + + it('charges one credit per contact returned by domain search', () => { + expect(cost(findEmailsByDomainTool, {}, { contacts: [{}, {}, {}] }).cost).toBeCloseTo( + 3 * FINDYMAIL_CREDIT_USD + ) + }) + + it('charges 2 credits for a reverse lookup with profile enrichment, 1 without', () => { + expect( + cost(reverseEmailLookupTool, { with_profile: true }, { email: 'a@b.com' }).cost + ).toBeCloseTo(2 * FINDYMAIL_CREDIT_USD) + expect( + cost(reverseEmailLookupTool, { with_profile: false }, { email: 'a@b.com' }).cost + ).toBeCloseTo(FINDYMAIL_CREDIT_USD) + expect( + cost(reverseEmailLookupTool, {}, { email: null, linkedin_url: null, fullName: null }).cost + ).toBe(0) + }) + + it('charges one verifier credit per verification', () => { + expect(cost(verifyEmailTool, {}, { verified: true }).cost).toBeCloseTo(FINDYMAIL_CREDIT_USD) + }) +}) + +describe('Prospeo hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(enrichPersonTool.hosting?.envKeyPrefix).toBe('PROSPEO_API_KEY') + expect(enrichPersonTool.hosting?.byokProviderId).toBe('prospeo') + }) + + it('charges 1 credit for a person match and 10 when a mobile is revealed', () => { + expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: {} }).cost).toBeCloseTo( + PROSPEO_CREDIT_USD + ) + expect( + cost(enrichPersonTool, {}, { free_enrichment: false, person: { mobile: { revealed: true } } }) + .cost + ).toBeCloseTo(10 * PROSPEO_CREDIT_USD) + }) + + it('does not charge on a free or no-match enrichment', () => { + expect(cost(enrichPersonTool, {}, { free_enrichment: true, person: {} }).cost).toBe(0) + expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: null }).cost).toBe(0) + expect(cost(enrichCompanyTool, {}, { free_enrichment: false, company: null }).cost).toBe(0) + }) + + it('uses the API-reported total_cost for bulk endpoints', () => { + expect(cost(bulkEnrichPersonTool, {}, { total_cost: 7 }).cost).toBeCloseTo( + 7 * PROSPEO_CREDIT_USD + ) + }) + + it('throws when bulk total_cost is missing', () => { + expect(() => cost(bulkEnrichPersonTool, {}, { matched: [] })).toThrow(/total_cost/) + }) + + it('charges one credit per non-free search page with results', () => { + expect(cost(searchPersonTool, {}, { free: false, results: [{}] }).cost).toBeCloseTo( + PROSPEO_CREDIT_USD + ) + expect(cost(searchPersonTool, {}, { free: true, results: [{}] }).cost).toBe(0) + expect(cost(searchPersonTool, {}, { free: false, results: [] }).cost).toBe(0) + }) +}) + +describe('Wiza hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(wizaIndividualRevealTool.hosting?.envKeyPrefix).toBe('WIZA_API_KEY') + expect(wizaIndividualRevealTool.hosting?.byokProviderId).toBe('wiza') + }) + + it('charges 2 credits for a valid email and 5 for a phone on individual reveal', () => { + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [] }).cost + ).toBeCloseTo(2 * WIZA_CREDIT_USD) + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', mobile_phone: '+1555' }).cost + ).toBeCloseTo(5 * WIZA_CREDIT_USD) + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [{ number: '+1555' }] }) + .cost + ).toBeCloseTo(7 * WIZA_CREDIT_USD) + expect(cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', phones: [] }).cost).toBe(0) + }) + + it('charges 2 credits per company enrichment match and nothing for prospect search', () => { + expect(cost(wizaCompanyEnrichmentTool, {}, { company_name: 'Wiza' }).cost).toBeCloseTo( + 2 * WIZA_CREDIT_USD + ) + expect( + cost( + wizaCompanyEnrichmentTool, + {}, + { company_name: null, company_domain: null, domain: null } + ).cost + ).toBe(0) + expect(cost(wizaProspectSearchTool, {}, { total: 100, profiles: [] }).cost).toBe(0) + }) + + it('polls the reveal to completion in postProcess', async () => { + vi.useFakeTimers() + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + data: { + id: 123, + status: 'finished', + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 123, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(2000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledWith( + 'https://wiza.co/api/individual_reveals/123', + expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer k' }) }) + ) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + expect((result.output as any).status).toBe('finished') + }) + + it('returns immediately without polling when the initial reveal is already terminal', async () => { + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + id: 123, + status: 'finished', + is_complete: true, + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + } as any, + } + const result = await wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + }) + + it('retries transient poll errors and still resolves on a later finished response', async () => { + vi.useFakeTimers() + const fetchMock = vi + .fn() + .mockResolvedValueOnce(new Response('busy', { status: 503 })) + .mockResolvedValueOnce(new Response('rate limited', { status: 429 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + data: { + id: 1, + status: 'finished', + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 1, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(6000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledTimes(3) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + }) + + it('returns an explicit failure (not a queued success) after repeated poll errors', async () => { + vi.useFakeTimers() + const fetchMock = vi.fn().mockResolvedValue(new Response('error', { status: 500 })) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 1, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(6000) + const result = await promise + + expect(result.success).toBe(false) + expect((result.output as any).status).toBe('queued') + }) +}) diff --git a/apps/sim/tools/findymail/find_email_from_linkedin.ts b/apps/sim/tools/findymail/find_email_from_linkedin.ts index 87eb569f960..5d095cd9cf4 100644 --- a/apps/sim/tools/findymail/find_email_from_linkedin.ts +++ b/apps/sim/tools/findymail/find_email_from_linkedin.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailFromLinkedInParams, FindymailFindEmailFromLinkedInResponse, @@ -15,6 +16,11 @@ export const findEmailFromLinkedInTool: ToolConfig< "Find someone's email from a LinkedIn profile URL or username. Uses one finder credit when a verified email is found.", version: '1.0.0', + hosting: findymailHosting((_params, output) => { + const contact = output.contact as { email?: string } | null + return contact?.email ? 1 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/find_email_from_name.ts b/apps/sim/tools/findymail/find_email_from_name.ts index 5a22a19c3b8..6a09ed75bd3 100644 --- a/apps/sim/tools/findymail/find_email_from_name.ts +++ b/apps/sim/tools/findymail/find_email_from_name.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailFromNameParams, FindymailFindEmailFromNameResponse, @@ -15,6 +16,11 @@ export const findEmailFromNameTool: ToolConfig< "Find someone's email from their name and a company domain or company name. Uses one finder credit when a verified email is found.", version: '1.0.0', + hosting: findymailHosting((_params, output) => { + const contact = output.contact as { email?: string } | null + return contact?.email ? 1 : 0 + }), + params: { name: { type: 'string', diff --git a/apps/sim/tools/findymail/find_emails_by_domain.ts b/apps/sim/tools/findymail/find_emails_by_domain.ts index ebb1de7ee3f..de26fd4f0a3 100644 --- a/apps/sim/tools/findymail/find_emails_by_domain.ts +++ b/apps/sim/tools/findymail/find_emails_by_domain.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailsByDomainParams, FindymailFindEmailsByDomainResponse, @@ -15,6 +16,15 @@ export const findEmailsByDomainTool: ToolConfig< 'Find verified contacts at a given domain matching one or more target roles (max 3 roles). Limited to 5 concurrent synchronous requests.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // No contacts array means no verified contacts returned — no charge. + if (!Array.isArray(output.contacts)) { + return 0 + } + // 1 finder credit per verified contact returned. + return output.contacts.length + }), + params: { domain: { type: 'string', diff --git a/apps/sim/tools/findymail/find_employees.ts b/apps/sim/tools/findymail/find_employees.ts index aabd08ce472..9051ad8d75e 100644 --- a/apps/sim/tools/findymail/find_employees.ts +++ b/apps/sim/tools/findymail/find_employees.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmployeesParams, FindymailFindEmployeesResponse, @@ -15,6 +16,15 @@ export const findEmployeesTool: ToolConfig< 'Find employees at a company by website and target job titles. Uses 1 credit per found contact. Does not return email addresses.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // No employees array means no contacts found — no charge. + if (!Array.isArray(output.employees)) { + return 0 + } + // 1 finder credit per contact found. + return output.employees.length + }), + params: { website: { type: 'string', diff --git a/apps/sim/tools/findymail/find_phone.ts b/apps/sim/tools/findymail/find_phone.ts index 0222eeed796..8a5c48f1591 100644 --- a/apps/sim/tools/findymail/find_phone.ts +++ b/apps/sim/tools/findymail/find_phone.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindPhoneParams, FindymailFindPhoneResponse } from '@/tools/findymail/types' import type { ToolConfig } from '@/tools/types' @@ -8,6 +9,11 @@ export const findPhoneTool: ToolConfig((_params, output) => { + // Phone lookups consume 10 finder credits, only when a number is found. + return output.phone ? 10 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/get_company.ts b/apps/sim/tools/findymail/get_company.ts index 2ee001f8527..64c03bcf831 100644 --- a/apps/sim/tools/findymail/get_company.ts +++ b/apps/sim/tools/findymail/get_company.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailGetCompanyParams, FindymailGetCompanyResponse, @@ -11,6 +12,11 @@ export const getCompanyTool: ToolConfig((_params, output) => { + // 1 finder credit per successful company match. + return output.name || output.domain ? 1 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/hosting.ts b/apps/sim/tools/findymail/hosting.ts new file mode 100644 index 00000000000..e624c13163c --- /dev/null +++ b/apps/sim/tools/findymail/hosting.ts @@ -0,0 +1,42 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Findymail hosted keys. Provide keys as + * `FINDYMAIL_API_KEY_COUNT` plus `FINDYMAIL_API_KEY_1..N`. + */ +export const FINDYMAIL_API_KEY_PREFIX = 'FINDYMAIL_API_KEY' + +/** + * Dollar cost of a single Findymail finder credit. + * + * Findymail charges per verified result: 1 credit per email, 10 credits per + * phone, and only when a result is found. Estimated from the $99/month Starter + * plan (5,000 credits ≈ $0.0198/credit) — https://www.findymail.com/pricing/. + */ +export const FINDYMAIL_CREDIT_USD = 0.02 + +/** + * Build a Findymail `hosting` config. `getCredits` returns the number of + * Findymail credits the call consumed, derived from the tool's output (per the + * documented per-endpoint credit model at https://www.findymail.com/api/). + */ +export function findymailHosting

    ( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

    { + return { + envKeyPrefix: FINDYMAIL_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'findymail', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * FINDYMAIL_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/findymail/lookup_technologies.ts b/apps/sim/tools/findymail/lookup_technologies.ts index b7fb2af8d1d..e7360f36151 100644 --- a/apps/sim/tools/findymail/lookup_technologies.ts +++ b/apps/sim/tools/findymail/lookup_technologies.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailLookupTechnologiesParams, FindymailLookupTechnologiesResponse, @@ -15,6 +16,11 @@ export const lookupTechnologiesTool: ToolConfig< 'Get the technology stack for a company by domain. Optionally filter by technology names. 1 finder credit if technologies are found, free otherwise.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // 1 finder credit when a technology stack is returned, free otherwise. + return Array.isArray(output.technologies) && output.technologies.length > 0 ? 1 : 0 + }), + params: { domain: { type: 'string', diff --git a/apps/sim/tools/findymail/reverse_email_lookup.ts b/apps/sim/tools/findymail/reverse_email_lookup.ts index 48a0c42ca18..447d112b696 100644 --- a/apps/sim/tools/findymail/reverse_email_lookup.ts +++ b/apps/sim/tools/findymail/reverse_email_lookup.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailReverseEmailLookupParams, FindymailReverseEmailLookupResponse, @@ -14,6 +15,13 @@ export const reverseEmailLookupTool: ToolConfig< 'Find a business profile from an email address. Uses 1 finder credit if a profile is found, 2 credits if returning full profile data.', version: '1.0.0', + hosting: findymailHosting((params, output) => { + const found = Boolean(output.email || output.linkedin_url || output.fullName) + if (!found) return 0 + // 1 credit for a match, 2 when full profile enrichment is requested. + return params.with_profile ? 2 : 1 + }), + params: { email: { type: 'string', diff --git a/apps/sim/tools/findymail/search_technologies.ts b/apps/sim/tools/findymail/search_technologies.ts index a4f7bcadcc5..64d56b785a3 100644 --- a/apps/sim/tools/findymail/search_technologies.ts +++ b/apps/sim/tools/findymail/search_technologies.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailSearchTechnologiesParams, FindymailSearchTechnologiesResponse, @@ -15,6 +16,11 @@ export const searchTechnologiesTool: ToolConfig< 'Search the technology catalog by name. Returns up to 25 technologies. Free endpoint, rate limited to 10 requests per minute.', version: '1.0.0', + hosting: findymailHosting(() => { + // Free catalog search — consumes no Findymail credits. + return 0 + }), + params: { q: { type: 'string', diff --git a/apps/sim/tools/findymail/verify_email.ts b/apps/sim/tools/findymail/verify_email.ts index c239e296cd0..30463e1fe6c 100644 --- a/apps/sim/tools/findymail/verify_email.ts +++ b/apps/sim/tools/findymail/verify_email.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailVerifyEmailParams, FindymailVerifyEmailResponse, @@ -11,6 +12,11 @@ export const verifyEmailTool: ToolConfig(() => { + // Each verification consumes one verifier credit, billed at the finder-credit rate. + return 1 + }), + params: { email: { type: 'string', diff --git a/apps/sim/tools/prospeo/bulk_enrich_company.ts b/apps/sim/tools/prospeo/bulk_enrich_company.ts index c73c6e70f12..f2023921629 100644 --- a/apps/sim/tools/prospeo/bulk_enrich_company.ts +++ b/apps/sim/tools/prospeo/bulk_enrich_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoBulkEnrichCompanyParams, @@ -15,6 +16,14 @@ export const bulkEnrichCompanyTool: ToolConfig< description: 'Enrich up to 50 company records at once.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // Prospeo reports the exact credits spent for the batch in total_cost. + if (typeof output.total_cost !== 'number') { + throw new Error('Prospeo bulk enrich company response missing total_cost') + } + return output.total_cost + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/bulk_enrich_person.ts b/apps/sim/tools/prospeo/bulk_enrich_person.ts index b6d095e9526..e594e9b7cb4 100644 --- a/apps/sim/tools/prospeo/bulk_enrich_person.ts +++ b/apps/sim/tools/prospeo/bulk_enrich_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoBulkEnrichPersonParams, @@ -15,6 +16,14 @@ export const bulkEnrichPersonTool: ToolConfig< description: 'Enrich up to 50 person records at once.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // Prospeo reports the exact credits spent for the batch in total_cost. + if (typeof output.total_cost !== 'number') { + throw new Error('Prospeo bulk enrich person response missing total_cost') + } + return output.total_cost + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/enrich_company.ts b/apps/sim/tools/prospeo/enrich_company.ts index ca4e1ed9b99..7f0b141e3d3 100644 --- a/apps/sim/tools/prospeo/enrich_company.ts +++ b/apps/sim/tools/prospeo/enrich_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoEnrichCompanyParams, @@ -14,6 +15,12 @@ export const enrichCompanyTool: ToolConfig< description: 'Enrich a company with complete B2B data.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // 1 credit per company match; no charge on a no-match or repeat enrichment. + if (output.free_enrichment === true) return 0 + return output.company ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/enrich_person.ts b/apps/sim/tools/prospeo/enrich_person.ts index 44c72a26007..e072dc6bbbf 100644 --- a/apps/sim/tools/prospeo/enrich_person.ts +++ b/apps/sim/tools/prospeo/enrich_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoEnrichPersonParams, @@ -12,6 +13,16 @@ export const enrichPersonTool: ToolConfig((_params, output) => { + // No charge on a no-match or a repeat enrichment. + if (output.free_enrichment === true) return 0 + const person = output.person as Record | null + if (!person) return 0 + // 10 credits when a mobile is revealed, otherwise 1 for the person match. + const mobile = person.mobile as { revealed?: boolean } | undefined + return mobile?.revealed ? 10 : 1 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/hosting.ts b/apps/sim/tools/prospeo/hosting.ts new file mode 100644 index 00000000000..c5d97b98475 --- /dev/null +++ b/apps/sim/tools/prospeo/hosting.ts @@ -0,0 +1,43 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Prospeo hosted keys. Provide keys as + * `PROSPEO_API_KEY_COUNT` plus `PROSPEO_API_KEY_1..N`. + */ +export const PROSPEO_API_KEY_PREFIX = 'PROSPEO_API_KEY' + +/** + * Dollar cost of a single Prospeo credit. + * + * Prospeo charges per match: 1 credit per person/company match, 10 credits when + * a mobile is revealed, and never on a no-match or a repeat enrichment. Based on + * the $39/month Starter plan (1,000 credits ≈ $0.039/credit) — https://prospeo.io/pricing. + */ +export const PROSPEO_CREDIT_USD = 0.039 + +/** + * Build a Prospeo `hosting` config. `getCredits` returns the number of Prospeo + * credits the call consumed, derived from the tool's output (prefer the + * API-reported `total_cost` for bulk endpoints; otherwise compute from the + * `free`/`free_enrichment` flag and the match). + */ +export function prospeoHosting

    ( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

    { + return { + envKeyPrefix: PROSPEO_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'prospeo', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * PROSPEO_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/prospeo/search_company.ts b/apps/sim/tools/prospeo/search_company.ts index f4e8a1c0b52..e16ecaf83c1 100644 --- a/apps/sim/tools/prospeo/search_company.ts +++ b/apps/sim/tools/prospeo/search_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, PROSPEO_PAGINATION_OUTPUT, @@ -16,6 +17,13 @@ export const searchCompanyTool: ToolConfig< description: 'Search for companies using 20+ filters to build account lists.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // 1 credit per page that returns at least one result; free on 30-day dedup. + if (output.free === true) return 0 + const results = output.results + return Array.isArray(results) && results.length > 0 ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/search_person.ts b/apps/sim/tools/prospeo/search_person.ts index c8415ceec9e..883b11e73dc 100644 --- a/apps/sim/tools/prospeo/search_person.ts +++ b/apps/sim/tools/prospeo/search_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, PROSPEO_PAGINATION_OUTPUT, @@ -14,6 +15,13 @@ export const searchPersonTool: ToolConfig((_params, output) => { + // 1 credit per page that returns at least one result; free on 30-day dedup. + if (output.free === true) return 0 + const results = output.results + return Array.isArray(results) && results.length > 0 ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3070576fa42..d6558b49423 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3053,9 +3053,8 @@ import { import { wizaCompanyEnrichmentTool, wizaGetCreditsTool, - wizaGetIndividualRevealTool, + wizaIndividualRevealTool, wizaProspectSearchTool, - wizaStartIndividualRevealTool, } from '@/tools/wiza' import { wordpressCreateCategoryTool, @@ -5436,9 +5435,8 @@ export const tools: Record = { wikipedia_random: wikipediaRandomPageTool, wiza_company_enrichment: wizaCompanyEnrichmentTool, wiza_get_credits: wizaGetCreditsTool, - wiza_get_individual_reveal: wizaGetIndividualRevealTool, + wiza_individual_reveal: wizaIndividualRevealTool, wiza_prospect_search: wizaProspectSearchTool, - wiza_start_individual_reveal: wizaStartIndividualRevealTool, wordpress_create_post: wordpressCreatePostTool, wordpress_update_post: wordpressUpdatePostTool, wordpress_delete_post: wordpressDeletePostTool, diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index b89ca98cd7e..24501cb6d96 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -21,6 +21,9 @@ export type BYOKProviderId = | 'cohere' | 'hunter' | 'peopledatalabs' + | 'findymail' + | 'prospeo' + | 'wiza' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' @@ -309,7 +312,7 @@ export type ToolHostingPricing

    > = PerRequestPricing * Adding more keys only requires updating the count and adding the new env var — * no code changes needed. */ -interface ToolHostingConfig

    > { +export interface ToolHostingConfig

    > { /** Optional predicate for tools where hosted keys only apply to some parameter combinations. */ enabled?: (params: P) => boolean /** diff --git a/apps/sim/tools/wiza/company_enrichment.ts b/apps/sim/tools/wiza/company_enrichment.ts index 70bfdfec5be..39089404f1c 100644 --- a/apps/sim/tools/wiza/company_enrichment.ts +++ b/apps/sim/tools/wiza/company_enrichment.ts @@ -1,4 +1,5 @@ import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' import type { WizaCompanyEnrichmentParams, WizaCompanyEnrichmentResponse } from '@/tools/wiza/types' export const wizaCompanyEnrichmentTool: ToolConfig< @@ -11,6 +12,11 @@ export const wizaCompanyEnrichmentTool: ToolConfig< 'Enrich a company by name, domain, LinkedIn ID, or LinkedIn slug with detailed firmographic data', version: '1.0.0', + hosting: wizaHosting((_params, output) => { + // 2 API credits per successful company match; no charge on a no-match. + return output.company_name || output.company_domain || output.domain ? 2 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/wiza/get_individual_reveal.ts b/apps/sim/tools/wiza/get_individual_reveal.ts deleted file mode 100644 index 7c3d4ec5434..00000000000 --- a/apps/sim/tools/wiza/get_individual_reveal.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { - WizaGetIndividualRevealParams, - WizaGetIndividualRevealResponse, -} from '@/tools/wiza/types' - -export const wizaGetIndividualRevealTool: ToolConfig< - WizaGetIndividualRevealParams, - WizaGetIndividualRevealResponse -> = { - id: 'wiza_get_individual_reveal', - name: 'Wiza Get Individual Reveal', - description: 'Retrieve the status and enriched data for an individual reveal by ID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Wiza API key', - }, - id: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Individual reveal ID returned from Start Individual Reveal', - }, - }, - - request: { - url: (params: WizaGetIndividualRevealParams) => - `https://wiza.co/api/individual_reveals/${encodeURIComponent(String(params.id).trim())}`, - method: 'GET', - headers: (params: WizaGetIndividualRevealParams) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', - }), - }, - - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Wiza API error: ${response.status} - ${errorText}`) - } - - const json = await response.json() - const d = json.data ?? {} - const emails = Array.isArray(d.emails) ? d.emails : [] - const phones = Array.isArray(d.phones) ? d.phones : [] - - return { - success: true, - output: { - id: d.id ?? null, - status: d.status ?? null, - is_complete: d.is_complete ?? null, - name: d.name ?? null, - company: d.company ?? null, - enrichment_level: d.enrichment_level ?? null, - linkedin_profile_url: d.linkedin_profile_url ?? null, - title: d.title ?? null, - location: d.location ?? null, - email: d.email ?? null, - email_type: d.email_type ?? null, - email_status: d.email_status ?? null, - emails: emails.map((e: Record) => ({ - email: (e.email as string) ?? null, - email_type: (e.email_type as string) ?? null, - email_status: (e.email_status as string) ?? null, - })), - mobile_phone: d.mobile_phone ?? null, - phone_number: d.phone_number ?? null, - phone_status: d.phone_status ?? null, - phones: phones.map((p: Record) => ({ - number: (p.number as string) ?? null, - pretty_number: (p.pretty_number as string) ?? null, - type: (p.type as string) ?? null, - })), - company_size: d.company_size ?? null, - company_size_range: d.company_size_range ?? null, - company_type: d.company_type ?? null, - company_domain: d.company_domain ?? null, - company_locality: d.company_locality ?? null, - company_region: d.company_region ?? null, - company_country: d.company_country ?? null, - company_street: d.company_street ?? null, - company_postal_code: d.company_postal_code ?? null, - company_founded: d.company_founded ?? null, - company_funding: d.company_funding ?? null, - company_revenue: d.company_revenue ?? null, - company_industry: d.company_industry ?? null, - company_subindustry: d.company_subindustry ?? null, - company_linkedin: d.company_linkedin ?? null, - company_location: d.company_location ?? null, - company_description: d.company_description ?? null, - credits: d.credits ?? null, - }, - } - }, - - outputs: { - id: { type: 'number', description: 'Reveal ID', optional: true }, - status: { - type: 'string', - description: 'queued | resolving | finished | failed', - optional: true, - }, - is_complete: { - type: 'boolean', - description: 'Whether the reveal has completed', - optional: true, - }, - name: { type: 'string', description: 'Full name', optional: true }, - company: { type: 'string', description: 'Company name', optional: true }, - enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true }, - linkedin_profile_url: { type: 'string', description: 'LinkedIn URL', optional: true }, - title: { type: 'string', description: 'Job title', optional: true }, - location: { type: 'string', description: 'Location', optional: true }, - email: { type: 'string', description: 'Primary email', optional: true }, - email_type: { type: 'string', description: 'Email type', optional: true }, - email_status: { type: 'string', description: 'valid | risky | unfound', optional: true }, - emails: { - type: 'array', - description: 'All emails found', - optional: true, - items: { - type: 'object', - properties: { - email: { type: 'string' }, - email_type: { type: 'string' }, - email_status: { type: 'string' }, - }, - }, - }, - mobile_phone: { type: 'string', description: 'Mobile phone', optional: true }, - phone_number: { type: 'string', description: 'Direct/office phone', optional: true }, - phone_status: { type: 'string', description: 'found | unfound', optional: true }, - phones: { - type: 'array', - description: 'All phones found', - optional: true, - items: { - type: 'object', - properties: { - number: { type: 'string' }, - pretty_number: { type: 'string' }, - type: { type: 'string' }, - }, - }, - }, - company_size: { type: 'number', description: 'Employee count', optional: true }, - company_size_range: { type: 'string', description: 'Headcount range', optional: true }, - company_type: { type: 'string', description: 'Company type', optional: true }, - company_domain: { type: 'string', description: 'Company domain', optional: true }, - company_locality: { type: 'string', description: 'City', optional: true }, - company_region: { type: 'string', description: 'State/region', optional: true }, - company_country: { type: 'string', description: 'Country', optional: true }, - company_street: { type: 'string', description: 'Street', optional: true }, - company_postal_code: { type: 'string', description: 'Postal code', optional: true }, - company_founded: { type: 'number', description: 'Year founded', optional: true }, - company_funding: { type: 'string', description: 'Funding total', optional: true }, - company_revenue: { type: 'string', description: 'Revenue', optional: true }, - company_industry: { type: 'string', description: 'Industry', optional: true }, - company_subindustry: { type: 'string', description: 'Subindustry', optional: true }, - company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, - company_location: { type: 'string', description: 'Full company location', optional: true }, - company_description: { type: 'string', description: 'Company description', optional: true }, - credits: { - type: 'json', - description: - 'Credits deducted for this reveal (api_credits: { total, email_credits, phone_credits, scrape_credits })', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/wiza/hosting.ts b/apps/sim/tools/wiza/hosting.ts new file mode 100644 index 00000000000..43fe367ae34 --- /dev/null +++ b/apps/sim/tools/wiza/hosting.ts @@ -0,0 +1,42 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Wiza hosted keys. Provide keys as `WIZA_API_KEY_COUNT` + * plus `WIZA_API_KEY_1..N`. + */ +export const WIZA_API_KEY_PREFIX = 'WIZA_API_KEY' + +/** + * Dollar cost of a single Wiza API credit. + * + * Wiza meters API usage in credits at a documented $0.025/credit (2,000-credit + * minimum) — https://help.wiza.co/en/articles/13551713-how-to-purchase-api-credits. + * Credits are deducted only when data is successfully returned: 2 credits per + * valid email, 5 credits per phone, 2 credits per company enrichment. + */ +export const WIZA_CREDIT_USD = 0.025 + +/** + * Build a Wiza `hosting` config. `getCredits` returns the number of Wiza API + * credits the call consumed, derived from the tool's output. + */ +export function wizaHosting

    ( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

    { + return { + envKeyPrefix: WIZA_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'wiza', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * WIZA_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/wiza/index.ts b/apps/sim/tools/wiza/index.ts index 21df6dec38d..289503b4da1 100644 --- a/apps/sim/tools/wiza/index.ts +++ b/apps/sim/tools/wiza/index.ts @@ -1,6 +1,5 @@ export { wizaCompanyEnrichmentTool } from './company_enrichment' export { wizaGetCreditsTool } from './get_credits' -export { wizaGetIndividualRevealTool } from './get_individual_reveal' +export { wizaIndividualRevealTool } from './individual_reveal' export { wizaProspectSearchTool } from './prospect_search' -export { wizaStartIndividualRevealTool } from './start_individual_reveal' export type * from './types' diff --git a/apps/sim/tools/wiza/individual_reveal.ts b/apps/sim/tools/wiza/individual_reveal.ts new file mode 100644 index 00000000000..291d1791f67 --- /dev/null +++ b/apps/sim/tools/wiza/individual_reveal.ts @@ -0,0 +1,330 @@ +import { sleep } from '@sim/utils/helpers' +import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' +import type { + WizaIndividualRevealData, + WizaIndividualRevealParams, + WizaIndividualRevealResponse, +} from '@/tools/wiza/types' + +const POLL_INTERVAL_MS = 2000 +const MAX_POLL_TIME_MS = 120000 +/** Tolerate brief Wiza outages while polling before giving up on an already-started reveal. */ +const MAX_CONSECUTIVE_POLL_ERRORS = 3 + +/** Whether a reveal payload has reached a terminal state and no longer needs polling. */ +function isTerminalReveal(d: { status?: string | null; is_complete?: boolean | null }): boolean { + return d.status === 'finished' || d.status === 'failed' || d.is_complete === true +} + +/** Map a Wiza individual-reveal payload (`data` object) to the tool output shape. */ +function mapRevealData(d: Record): WizaIndividualRevealData { + const emails = Array.isArray(d.emails) ? (d.emails as Record[]) : [] + const phones = Array.isArray(d.phones) ? (d.phones as Record[]) : [] + return { + id: (d.id as number) ?? null, + status: (d.status as string) ?? null, + is_complete: (d.is_complete as boolean) ?? null, + name: (d.name as string) ?? null, + company: (d.company as string) ?? null, + enrichment_level: (d.enrichment_level as string) ?? null, + linkedin_profile_url: (d.linkedin_profile_url as string) ?? null, + title: (d.title as string) ?? null, + location: (d.location as string) ?? null, + email: (d.email as string) ?? null, + email_type: (d.email_type as string) ?? null, + email_status: (d.email_status as string) ?? null, + emails: emails.map((e) => ({ + email: (e.email as string) ?? null, + email_type: (e.email_type as string) ?? null, + email_status: (e.email_status as string) ?? null, + })), + mobile_phone: (d.mobile_phone as string) ?? null, + phone_number: (d.phone_number as string) ?? null, + phone_status: (d.phone_status as string) ?? null, + phones: phones.map((p) => ({ + number: (p.number as string) ?? null, + pretty_number: (p.pretty_number as string) ?? null, + type: (p.type as string) ?? null, + })), + company_size: (d.company_size as number) ?? null, + company_size_range: (d.company_size_range as string) ?? null, + company_type: (d.company_type as string) ?? null, + company_domain: (d.company_domain as string) ?? null, + company_locality: (d.company_locality as string) ?? null, + company_region: (d.company_region as string) ?? null, + company_country: (d.company_country as string) ?? null, + company_street: (d.company_street as string) ?? null, + company_postal_code: (d.company_postal_code as string) ?? null, + company_founded: (d.company_founded as number) ?? null, + company_funding: (d.company_funding as string) ?? null, + company_revenue: (d.company_revenue as string) ?? null, + company_industry: (d.company_industry as string) ?? null, + company_subindustry: (d.company_subindustry as string) ?? null, + company_linkedin: (d.company_linkedin as string) ?? null, + company_location: (d.company_location as string) ?? null, + company_description: (d.company_description as string) ?? null, + credits: (d.credits as Record) ?? null, + } +} + +export const wizaIndividualRevealTool: ToolConfig< + WizaIndividualRevealParams, + WizaIndividualRevealResponse +> = { + id: 'wiza_individual_reveal', + name: 'Wiza Individual Reveal', + description: + 'Reveal a contact via LinkedIn URL, name + company/domain, or email. Starts the reveal and polls until it resolves. Uses 2 credits per valid email and 5 credits per phone, charged only on success.', + version: '1.0.0', + + hosting: wizaHosting((_params, output) => { + let credits = 0 + const emails = Array.isArray(output.emails) + ? (output.emails as { email_status?: string }[]) + : [] + const emailValid = + output.email_status === 'valid' || emails.some((e) => e.email_status === 'valid') + // 2 credits when at least one valid email is returned. + if (emailValid) credits += 2 + const phones = Array.isArray(output.phones) ? output.phones : [] + const phoneFound = Boolean(output.mobile_phone || output.phone_number || phones.length > 0) + // 5 credits when at least one phone is returned. + if (phoneFound) credits += 5 + return credits + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Wiza API key', + }, + enrichment_level: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Enrichment depth: none, partial, phone, or full', + }, + profile_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe)', + }, + full_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name (used with company or domain)', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name (used with full_name)', + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company domain (used with full_name)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address (use alone or with other identifiers)', + }, + accept_work: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to accept work emails (email_options)', + }, + accept_personal: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to accept personal emails (email_options)', + }, + }, + + request: { + url: 'https://wiza.co/api/individual_reveals', + method: 'POST', + headers: (params: WizaIndividualRevealParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: WizaIndividualRevealParams) => { + const individual: Record = {} + if (params.profile_url) individual.profile_url = params.profile_url + if (params.full_name) individual.full_name = params.full_name + if (params.company) individual.company = params.company + if (params.domain) individual.domain = params.domain + if (params.email) individual.email = params.email + + const body: Record = { + individual_reveal: individual, + enrichment_level: params.enrichment_level, + } + + if (params.accept_work !== undefined || params.accept_personal !== undefined) { + const emailOptions: Record = {} + if (params.accept_work !== undefined) emailOptions.accept_work = params.accept_work + if (params.accept_personal !== undefined) { + emailOptions.accept_personal = params.accept_personal + } + body.email_options = emailOptions + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Wiza API error: ${response.status} - ${errorText}`) + } + const json = await response.json() + return { + success: true, + output: mapRevealData(json.data ?? {}), + } + }, + + postProcess: async (result, params) => { + if (!result.success) return result + + // Wiza can resolve synchronously (e.g. a cache hit) — the initial POST payload is + // already mapped, so skip polling when it is terminal. + if (isTerminalReveal(result.output)) { + return { success: result.output.status !== 'failed', output: result.output } + } + + const revealId = result.output.id + if (revealId == null) { + // Return an explicit failure rather than throwing: a thrown error here is swallowed + // by the executor and masked as the queued (incomplete) success result. + return { + success: false, + error: 'Wiza individual reveal did not return an id', + output: result.output, + } + } + + let elapsedTime = 0 + let consecutiveErrors = 0 + while (elapsedTime < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsedTime += POLL_INTERVAL_MS + + const statusResponse = await fetch( + `https://wiza.co/api/individual_reveals/${encodeURIComponent(String(revealId))}`, + { + headers: { + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!statusResponse.ok) { + // The reveal is already started (and billed by Wiza), so tolerate brief outages and + // retry rather than aborting the whole window on a single transient 5xx/429. + consecutiveErrors += 1 + if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) { + const errorText = await statusResponse.text().catch(() => '') + return { + success: false, + error: `Wiza API error: ${statusResponse.status} - ${errorText}`, + output: result.output, + } + } + continue + } + consecutiveErrors = 0 + + const json = await statusResponse.json() + const data = json.data ?? {} + + if (isTerminalReveal(data)) { + return { + success: data.status !== 'failed', + output: mapRevealData(data), + } + } + } + + return { + success: false, + error: 'Wiza individual reveal did not complete within the polling window', + output: result.output, + } + }, + + outputs: { + id: { type: 'number', description: 'Reveal ID' }, + status: { type: 'string', description: 'queued | resolving | finished | failed' }, + is_complete: { type: 'boolean', description: 'Whether the reveal has completed' }, + name: { type: 'string', description: 'Full name', optional: true }, + company: { type: 'string', description: 'Company name', optional: true }, + enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true }, + linkedin_profile_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + location: { type: 'string', description: 'Location', optional: true }, + email: { type: 'string', description: 'Primary email', optional: true }, + email_type: { type: 'string', description: 'Email type', optional: true }, + email_status: { type: 'string', description: 'valid | risky | unfound', optional: true }, + emails: { + type: 'array', + description: 'All emails found', + optional: true, + items: { + type: 'object', + properties: { + email: { type: 'string' }, + email_type: { type: 'string' }, + email_status: { type: 'string' }, + }, + }, + }, + mobile_phone: { type: 'string', description: 'Mobile phone', optional: true }, + phone_number: { type: 'string', description: 'Direct/office phone', optional: true }, + phone_status: { type: 'string', description: 'found | unfound', optional: true }, + phones: { + type: 'array', + description: 'All phones found', + optional: true, + items: { + type: 'object', + properties: { + number: { type: 'string' }, + pretty_number: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + company_size: { type: 'number', description: 'Employee count', optional: true }, + company_size_range: { type: 'string', description: 'Headcount range', optional: true }, + company_type: { type: 'string', description: 'Company type', optional: true }, + company_domain: { type: 'string', description: 'Company domain', optional: true }, + company_locality: { type: 'string', description: 'City', optional: true }, + company_region: { type: 'string', description: 'State/region', optional: true }, + company_country: { type: 'string', description: 'Country', optional: true }, + company_street: { type: 'string', description: 'Street', optional: true }, + company_postal_code: { type: 'string', description: 'Postal code', optional: true }, + company_founded: { type: 'number', description: 'Year founded', optional: true }, + company_funding: { type: 'string', description: 'Funding total', optional: true }, + company_revenue: { type: 'string', description: 'Revenue', optional: true }, + company_industry: { type: 'string', description: 'Industry', optional: true }, + company_subindustry: { type: 'string', description: 'Subindustry', optional: true }, + company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, + company_location: { type: 'string', description: 'Full company location', optional: true }, + company_description: { type: 'string', description: 'Company description', optional: true }, + credits: { type: 'json', description: 'Credits consumed by the reveal', optional: true }, + }, +} diff --git a/apps/sim/tools/wiza/prospect_search.ts b/apps/sim/tools/wiza/prospect_search.ts index c507f0cbb61..fb460b39b63 100644 --- a/apps/sim/tools/wiza/prospect_search.ts +++ b/apps/sim/tools/wiza/prospect_search.ts @@ -1,4 +1,5 @@ import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' import type { WizaProspectSearchParams, WizaProspectSearchResponse } from '@/tools/wiza/types' export const wizaProspectSearchTool: ToolConfig< @@ -10,6 +11,12 @@ export const wizaProspectSearchTool: ToolConfig< description: "Search Wiza's database of prospects using person, company, and financial filters", version: '1.0.0', + hosting: wizaHosting(() => { + // Prospect search returns profiles without contact data and consumes no credits; + // Wiza charges only on reveal/enrichment. + return 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/wiza/start_individual_reveal.ts b/apps/sim/tools/wiza/start_individual_reveal.ts deleted file mode 100644 index 902c51f8723..00000000000 --- a/apps/sim/tools/wiza/start_individual_reveal.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { - WizaStartIndividualRevealParams, - WizaStartIndividualRevealResponse, -} from '@/tools/wiza/types' - -export const wizaStartIndividualRevealTool: ToolConfig< - WizaStartIndividualRevealParams, - WizaStartIndividualRevealResponse -> = { - id: 'wiza_start_individual_reveal', - name: 'Wiza Start Individual Reveal', - description: - 'Start an individual reveal to enrich a contact via LinkedIn URL, name+company, or email', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Wiza API key', - }, - enrichment_level: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Enrichment depth: none, partial, phone, or full', - }, - profile_url: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe)', - }, - full_name: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Full name (used with company or domain)', - }, - company: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Company name (used with full_name)', - }, - domain: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Company domain (used with full_name)', - }, - email: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Email address (use alone or with other identifiers)', - }, - accept_work: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether to accept work emails (email_options)', - }, - accept_personal: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether to accept personal emails (email_options)', - }, - callback_url: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Optional URL to receive a callback with the reveal update', - }, - }, - - request: { - url: 'https://wiza.co/api/individual_reveals', - method: 'POST', - headers: (params: WizaStartIndividualRevealParams) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', - }), - body: (params: WizaStartIndividualRevealParams) => { - const individual: Record = {} - if (params.profile_url) individual.profile_url = params.profile_url - if (params.full_name) individual.full_name = params.full_name - if (params.company) individual.company = params.company - if (params.domain) individual.domain = params.domain - if (params.email) individual.email = params.email - - const body: Record = { - individual_reveal: individual, - enrichment_level: params.enrichment_level, - } - - if (params.accept_work !== undefined || params.accept_personal !== undefined) { - const emailOptions: Record = {} - if (params.accept_work !== undefined) emailOptions.accept_work = params.accept_work - if (params.accept_personal !== undefined) { - emailOptions.accept_personal = params.accept_personal - } - body.email_options = emailOptions - } - - if (params.callback_url) body.callback_url = params.callback_url - - return body - }, - }, - - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Wiza API error: ${response.status} - ${errorText}`) - } - - const json = await response.json() - const d = json.data ?? {} - - return { - success: true, - output: { - id: d.id ?? null, - status: d.status ?? null, - is_complete: d.is_complete ?? null, - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'Individual reveal ID (use with Get Individual Reveal)', - optional: true, - }, - status: { - type: 'string', - description: 'Reveal status: queued, resolving, finished, or failed', - optional: true, - }, - is_complete: { - type: 'boolean', - description: 'Whether the reveal has completed', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/wiza/types.ts b/apps/sim/tools/wiza/types.ts index 00db0a23563..603a3fa933f 100644 --- a/apps/sim/tools/wiza/types.ts +++ b/apps/sim/tools/wiza/types.ts @@ -105,7 +105,7 @@ export interface WizaCompanyEnrichmentResponse extends ToolResponse { } } -export interface WizaStartIndividualRevealParams { +export interface WizaIndividualRevealParams { apiKey: string enrichment_level: 'none' | 'partial' | 'phone' | 'full' profile_url?: string @@ -115,10 +115,9 @@ export interface WizaStartIndividualRevealParams { email?: string accept_work?: boolean accept_personal?: boolean - callback_url?: string } -interface WizaIndividualRevealData { +export interface WizaIndividualRevealData { id: number | null status: string | null is_complete: boolean | null @@ -164,20 +163,7 @@ interface WizaIndividualRevealData { credits: Record | null } -export interface WizaStartIndividualRevealResponse extends ToolResponse { - output: { - id: number | null - status: string | null - is_complete: boolean | null - } -} - -export interface WizaGetIndividualRevealParams { - apiKey: string - id: string -} - -export interface WizaGetIndividualRevealResponse extends ToolResponse { +export interface WizaIndividualRevealResponse extends ToolResponse { output: WizaIndividualRevealData } @@ -185,5 +171,4 @@ export type WizaResponse = | WizaGetCreditsResponse | WizaProspectSearchResponse | WizaCompanyEnrichmentResponse - | WizaStartIndividualRevealResponse - | WizaGetIndividualRevealResponse + | WizaIndividualRevealResponse diff --git a/bun.lock b/bun.lock index fdee6edfc42..070992c9a4f 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/packages/db/.env.swp b/packages/db/.env.swp new file mode 100644 index 00000000000..27153b0737c Binary files /dev/null and b/packages/db/.env.swp differ diff --git a/packages/db/migrations/0219_amused_leo.sql b/packages/db/migrations/0219_amused_leo.sql new file mode 100644 index 00000000000..c32c029caca --- /dev/null +++ b/packages/db/migrations/0219_amused_leo.sql @@ -0,0 +1,19 @@ +ALTER TABLE "copilot_messages" ADD COLUMN "seq" integer;--> statement-breakpoint +WITH ordered AS ( + SELECT c."id" AS chat_id, elem.value->>'id' AS message_id, elem.ord AS ord + FROM "copilot_chats" c + CROSS JOIN LATERAL jsonb_array_elements(c."messages") WITH ORDINALITY AS elem(value, ord) + WHERE jsonb_typeof(c."messages") = 'array' AND jsonb_array_length(c."messages") > 0 +), +first_occurrence AS ( + SELECT chat_id, message_id, MIN(ord) AS first_ord FROM ordered GROUP BY chat_id, message_id +), +ranked AS ( + SELECT chat_id, message_id, + (ROW_NUMBER() OVER (PARTITION BY chat_id ORDER BY first_ord) - 1) AS seq + FROM first_occurrence +) +UPDATE "copilot_messages" m SET "seq" = r.seq +FROM ranked r +WHERE m."chat_id" = r.chat_id AND m."message_id" = r.message_id;--> statement-breakpoint +CREATE INDEX "copilot_messages_chat_seq_idx" ON "copilot_messages" USING btree ("chat_id","seq") WHERE "copilot_messages"."deleted_at" IS NULL; \ No newline at end of file diff --git a/packages/db/migrations/0220_early_hellion.sql b/packages/db/migrations/0220_early_hellion.sql new file mode 100644 index 00000000000..558f964c2d9 --- /dev/null +++ b/packages/db/migrations/0220_early_hellion.sql @@ -0,0 +1,36 @@ +ALTER TABLE "workflow_execution_logs" ADD COLUMN IF NOT EXISTS "cost_total" numeric;--> statement-breakpoint +ALTER TABLE "workflow_execution_logs" ADD COLUMN IF NOT EXISTS "models_used" text[];--> statement-breakpoint +COMMIT;--> statement-breakpoint +ALTER TYPE "public"."usage_log_category" ADD VALUE IF NOT EXISTS 'tool';--> statement-breakpoint +CREATE OR REPLACE PROCEDURE backfill_wel_cost_total_0220() LANGUAGE plpgsql AS $$ +DECLARE + updated integer; +BEGIN + LOOP + WITH candidates AS ( + SELECT id FROM workflow_execution_logs + WHERE cost_total IS NULL + AND cost ? 'total' + AND (cost->>'total') ~ '^-?[0-9]+(\.[0-9]+)?$' + LIMIT 5000 + ) + UPDATE workflow_execution_logs wel + SET cost_total = NULLIF(wel.cost->>'total', '')::numeric, + models_used = CASE + WHEN jsonb_typeof(wel.cost->'models') = 'object' + THEN ARRAY(SELECT jsonb_object_keys(wel.cost->'models')) + ELSE wel.models_used + END + FROM candidates + WHERE wel.id = candidates.id; + GET DIAGNOSTICS updated = ROW_COUNT; + EXIT WHEN updated = 0; + COMMIT; + END LOOP; +END; +$$;--> statement-breakpoint +CALL backfill_wel_cost_total_0220();--> statement-breakpoint +DROP PROCEDURE backfill_wel_cost_total_0220();--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "usage_log_execution_id_idx" ON "usage_log" USING btree ("execution_id");--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_workspace_cost_total_idx" ON "workflow_execution_logs" USING btree ("workspace_id","cost_total");--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_models_used_idx" ON "workflow_execution_logs" USING gin ("models_used"); diff --git a/packages/db/migrations/meta/0219_snapshot.json b/packages/db/migrations/meta/0219_snapshot.json new file mode 100644 index 00000000000..0205b4d34e2 --- /dev/null +++ b/packages/db/migrations/meta/0219_snapshot.json @@ -0,0 +1,17519 @@ +{ + "id": "9c04ddff-9332-4201-be6f-98fbf006874a", + "prevId": "7718b87c-bd20-48e1-8a85-28bdd43a8bc4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "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, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "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": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_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, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "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": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_name_unique": { + "name": "permission_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "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": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "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": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/0220_snapshot.json b/packages/db/migrations/meta/0220_snapshot.json new file mode 100644 index 00000000000..64c6073d85c --- /dev/null +++ b/packages/db/migrations/meta/0220_snapshot.json @@ -0,0 +1,17582 @@ +{ + "id": "10027d61-99fa-4082-99a8-27be35fc3d8a", + "prevId": "9c04ddff-9332-4201-be6f-98fbf006874a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "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, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "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": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_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, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "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": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_name_unique": { + "name": "permission_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "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": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "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": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 32d0ae5f16d..f348c087a60 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1527,6 +1527,20 @@ "when": 1779996122972, "tag": "0218_chubby_zzzax", "breakpoints": true + }, + { + "idx": 219, + "version": "7", + "when": 1780079220753, + "tag": "0219_amused_leo", + "breakpoints": true + }, + { + "idx": 220, + "version": "7", + "when": 1780081787541, + "tag": "0220_early_hellion", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 0c767dcece9..4e9b691a4ce 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -329,8 +329,25 @@ export const workflowExecutionLogs = pgTable( endedAt: timestamp('ended_at'), totalDurationMs: integer('total_duration_ms'), + /** + * Heavy trace data (traceSpans, finalOutput, workflowInput, executionState) + * is externalized to object storage; this column then holds a slim payload: + * a `traceStoreRef` (__simLargeValueRef) pointer to the stored object plus + * inline markers (hasTraceSpans, traceSpanCount, environment, trigger, + * truncation flags). It also still holds the FULL payload inline for legacy + * / not-yet-backfilled rows, for the storage-write-failure fallback, and for + * job_execution_logs. Required — not droppable. Read it via + * `materializeExecutionData`, which resolves the pointer. + */ executionData: jsonb('execution_data').notNull().default('{}'), + /** @deprecated Not written/read; cost lives in usage_log + the `cost_total` projection. Drop in a follow-up PR after the `cost_total` backfill. */ cost: jsonb('cost'), + // Faithful, write-once projection of the run's usage_log ledger sum (dollars). + // Backs list cost display/filter/sort without live aggregation; never an + // independently-computed value (cost_total == SUM(usage_log) for the run). + costTotal: decimal('cost_total'), + // Model names used by the run (incl. zero-cost/BYOK), for the v1 model filter. + modelsUsed: text('models_used').array(), files: jsonb('files'), // File metadata for execution files createdAt: timestamp('created_at').notNull().defaultNow(), }, @@ -356,6 +373,11 @@ export const workflowExecutionLogs = pgTable( table.workspaceId, table.startedAt ), + workspaceCostTotalIdx: index('workflow_execution_logs_workspace_cost_total_idx').on( + table.workspaceId, + table.costTotal + ), + modelsUsedIdx: index('workflow_execution_logs_models_used_idx').using('gin', table.modelsUsed), workspaceEndedAtIdIdx: index('workflow_execution_logs_workspace_ended_at_id_idx').on( table.workspaceId, sql`date_trunc('milliseconds', ${table.endedAt})`, @@ -881,39 +903,33 @@ export const userStats = pgTable('user_stats', { .notNull() .references(() => user.id, { onDelete: 'cascade' }) .unique(), // One record per user - /** - * Deprecated former usage hot-path counters. - * - * These used to be incremented from execution/API/trigger/chat/MCP/A2A - * billing paths on every usage event. New usage reporting must derive - * these dimensions from `usage_log` instead of writing this row. - */ + // Retired usage hot-path counters: no writers/readers; derive from usage_log. + // Drop via DROP COLUMN in a follow-up migration. + /** @deprecated Retired usage counter; derive from usage_log. */ totalManualExecutions: integer('total_manual_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalApiCalls: integer('total_api_calls').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalWebhookTriggers: integer('total_webhook_triggers').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalScheduledExecutions: integer('total_scheduled_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalChatExecutions: integer('total_chat_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalMcpExecutions: integer('total_mcp_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalA2aExecutions: integer('total_a2a_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalTokensUsed: bigint('total_tokens_used', { mode: 'number' }).notNull().default(0), - /** - * Deprecated former usage hot-path cost aggregate. - * - * `recordUsage` now appends attributed rows to `usage_log`; this column is - * retained only for legacy/admin reporting until consumers move to ledger - * aggregations. - */ + /** @deprecated Not written (recordUsage appends to usage_log); legacy/admin reads only. Move readers to ledger aggregation. */ totalCost: decimal('total_cost').notNull().default('0'), currentUsageLimit: decimal('current_usage_limit').default(DEFAULT_FREE_CREDITS.toString()), // Default $5 (1,000 credits) for free plan, null for team/enterprise usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(), /** - * Deprecated former usage hot-path current-period aggregate. - * - * Keep only as the pre-shift baseline for the active billing period. - * Canonical current-period usage is `currentPeriodCost` baseline plus - * attributed `usage_log` rows for the same billing entity and period. + * Active per-period baseline (not a per-usage hot-path counter). Current usage + * = this baseline + attributed usage_log rows for the period; reset at rollover. */ - currentPeriodCost: decimal('current_period_cost').notNull().default('0'), // Usage in current billing period + currentPeriodCost: decimal('current_period_cost').notNull().default('0'), lastPeriodCost: decimal('last_period_cost').default('0'), // Usage from previous billing period /** * Threshold/final billing tracker. @@ -933,25 +949,21 @@ export const userStats = pgTable('user_stats', { * overage collection. It is not a per-usage aggregate counter. */ creditBalance: decimal('credit_balance').notNull().default('0'), - /** - * Deprecated former Copilot hot-path cost/counter aggregates. - * - * Copilot/MCP Copilot usage should be reported from `usage_log` going - * forward. Current/last period Copilot columns remain as legacy reset - * trackers until those consumers are migrated. - */ + /** @deprecated Not written; report Copilot cost from usage_log. Legacy/admin reads only. */ totalCopilotCost: decimal('total_copilot_cost').notNull().default('0'), + /** Active per-period Copilot baseline; reset at rollover (not a per-usage counter). */ currentPeriodCopilotCost: decimal('current_period_copilot_cost').notNull().default('0'), + /** Previous-period Copilot cost; set at rollover. */ lastPeriodCopilotCost: decimal('last_period_copilot_cost').default('0'), + /** @deprecated Not written; report Copilot tokens from usage_log. Legacy/admin reads only. */ totalCopilotTokens: bigint('total_copilot_tokens', { mode: 'number' }).notNull().default(0), + /** @deprecated Not written; report Copilot calls from usage_log. Legacy/admin reads only. */ totalCopilotCalls: integer('total_copilot_calls').notNull().default(0), - /** - * Deprecated former MCP Copilot hot-path aggregates. - * - * New MCP Copilot billing usage should be reported from `usage_log`. - */ + /** @deprecated Not written; report MCP Copilot calls from usage_log. Legacy/admin reads only. */ totalMcpCopilotCalls: integer('total_mcp_copilot_calls').notNull().default(0), + /** @deprecated Not written; report MCP Copilot cost from usage_log. Legacy/admin reads only. */ totalMcpCopilotCost: decimal('total_mcp_copilot_cost').notNull().default('0'), + /** @deprecated No writer (never incremented or reset). MCP copilot usage lives in usage_log (source 'mcp_copilot'); read it from there, not this column. */ currentPeriodMcpCopilotCost: decimal('current_period_mcp_copilot_cost').notNull().default('0'), /** * Storage upload/delete hot-path tracker for personal plans. @@ -960,13 +972,7 @@ export const userStats = pgTable('user_stats', { * org-scoped storage writes update `organization.storageUsedBytes`. */ storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), - /** - * Deprecated former execution hot-path activity timestamp. - * - * Successful workflow execution no longer updates `user_stats`; this column - * is retained only for legacy/admin reporting until replaced by an activity - * source that does not contend on this row. - */ + /** @deprecated Not updated by execution (no user_stats write on completion); legacy/admin reads only. */ lastActive: timestamp('last_active').notNull().defaultNow(), billingBlocked: boolean('billing_blocked').notNull().default(false), billingBlockedReason: billingBlockedReasonEnum('billing_blocked_reason'), @@ -1985,6 +1991,7 @@ export const copilotMessages = pgTable( model: text('model'), tokensIn: integer('tokens_in'), tokensOut: integer('tokens_out'), + seq: integer('seq'), deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -1997,6 +2004,9 @@ export const copilotMessages = pgTable( chatCreatedAtIdx: index('copilot_messages_chat_created_at_idx') .on(table.chatId, table.createdAt, table.id) .where(sql`${table.deletedAt} IS NULL`), + chatSeqIdx: index('copilot_messages_chat_seq_idx') + .on(table.chatId, table.seq) + .where(sql`${table.deletedAt} IS NULL`), chatStreamIdx: index('copilot_messages_chat_stream_idx') .on(table.chatId, table.streamId) .where(sql`${table.streamId} IS NOT NULL`), @@ -2787,7 +2797,7 @@ export const auditLog = pgTable( }) ) -export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed']) +export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed', 'tool']) export const usageLogSourceEnum = pgEnum('usage_log_source', [ 'workflow', 'wand', @@ -2857,6 +2867,7 @@ export const usageLog = pgTable( table.workspaceId, table.createdAt ), + executionIdIdx: index('usage_log_execution_id_idx').on(table.executionId), }) ) diff --git a/packages/db/scripts/migrate.ts b/packages/db/scripts/migrate.ts index d8449a5775a..9d967a9db7c 100644 --- a/packages/db/scripts/migrate.ts +++ b/packages/db/scripts/migrate.ts @@ -2,6 +2,33 @@ import { drizzle } from 'drizzle-orm/postgres-js' import { migrate } from 'drizzle-orm/postgres-js/migrator' import postgres from 'postgres' +/** + * Concurrent-index convention (avoid write-blocking index builds on large tables) + * -------------------------------------------------------------------------------- + * drizzle-kit emits plain `CREATE INDEX`, which takes a SHARE lock and blocks all + * writes for the build duration — on a big, write-hot table (e.g. + * workflow_execution_logs, usage_log) that stalls every in-flight workflow + * completion for minutes. drizzle wraps each migration in a transaction, and + * `CREATE INDEX CONCURRENTLY` cannot run inside a transaction block. + * + * So, after generating a migration that adds an index on a large/hot table, edit + * the generated SQL to end drizzle's transaction first, then build concurrently + * and idempotently: + * + * COMMIT;--> statement-breakpoint + * CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_name" ON "table" (...); + * + * Notes: + * - Put the `COMMIT` breakpoint AFTER all transactional DDL (ALTER TABLE/TYPE) + * in the file and only the concurrent CREATE INDEX statements below it. + * - Use `IF NOT EXISTS` (and make sibling DDL idempotent, e.g. + * `ADD COLUMN IF NOT EXISTS`, `ADD VALUE IF NOT EXISTS`) so a re-run after a + * failed CONCURRENTLY build is safe — fresh DBs and re-applies both work. + * - CONCURRENTLY only takes a SHARE UPDATE EXCLUSIVE lock (allows reads/writes). + * - Always validate on staging before prod; a failed CONCURRENTLY build can + * leave an INVALID index that must be dropped and rebuilt. + */ + const url = process.env.DATABASE_URL if (!url) { console.error('ERROR: Missing DATABASE_URL environment variable.') @@ -12,6 +39,8 @@ if (!url) { const client = postgres(url, { max: 1, connect_timeout: 10 }) try { + // statement_timeout=0: index builds (esp. CONCURRENTLY on large tables) can run + // far longer than the app default; a migration must never be killed mid-build. await client`SET statement_timeout = 0` await migrate(drizzle(client), { migrationsFolder: './migrations' }) console.log('Migrations applied successfully.') diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 08a10338af8..e148296ab24 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -749,6 +749,7 @@ export const schemaMock = { model: 'model', tokensIn: 'tokensIn', tokensOut: 'tokensOut', + seq: 'seq', deletedAt: 'deletedAt', createdAt: 'createdAt', updatedAt: 'updatedAt', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 5b321a818d0..4fb99356eed 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -83,6 +83,14 @@ const INDIRECT_ZOD_ROUTES = new Set([ // that closes the popup, so the JSON-mode contract framework doesn't fit. // Validation is enforced via state lookup + session-vs-row userId match. 'apps/sim/app/api/mcp/oauth/callback/route.ts', + // Deprecated Copilot MCP surface: these routes are gated to always return + // 410 Gone and consume no client-supplied input. + 'apps/sim/app/api/mcp/copilot/route.ts', + 'apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts', + 'apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts', + // Deprecated v1 headless copilot chat API: gated to always return 410 Gone + // and consumes no client-supplied input. + 'apps/sim/app/api/v1/copilot/chat/route.ts', ]) /** @@ -110,7 +118,6 @@ const RAW_JSON_BASELINE_ROUTES = new Set([ 'apps/sim/app/api/invitations/[id]/route.ts', 'apps/sim/app/api/knowledge/[id]/documents/route.ts', 'apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts', - 'apps/sim/app/api/mcp/copilot/route.ts', 'apps/sim/app/api/mcp/serve/[serverId]/route.ts', 'apps/sim/app/api/mcp/servers/route.ts', 'apps/sim/app/api/mcp/servers/[id]/route.ts', diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index c2c50cbbc6a..9b806264a4d 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -1,7 +1,7 @@ #!/usr/bin/env ts-node import fs from 'fs' import path from 'path' -import { fileURLToPath } from 'url' +import { fileURLToPath, pathToFileURL } from 'url' import { glob } from 'glob' console.log('Starting documentation generator...') @@ -183,6 +183,7 @@ interface IntegrationEntry { category: string integrationTypes?: string[] tags?: string[] + landingContent?: Record } /** @@ -665,6 +666,19 @@ async function writeIntegrationsJson(iconMapping: Record): Promi const triggerRegistry = await buildTriggerRegistry() const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap() + + // Hand-authored, integration-specific landing content (install walkthrough, + // privacy blurb), keyed by slug. Imported as pure data — its only import is + // type-only and erased at runtime — and baked into the entries below so the + // landing page reads a single source instead of augmenting at render time. + const landingContentModule = await import( + pathToFileURL(path.join(LANDING_INTEGRATIONS_DATA_PATH, 'landing-content.ts')).href + ) + const landingContentMap = (landingContentModule.INTEGRATION_LANDING_CONTENT ?? {}) as Record< + string, + Record + > + const integrations: IntegrationEntry[] = [] const seenBaseTypes = new Set() const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort() @@ -778,6 +792,7 @@ async function writeIntegrationsJson(iconMapping: Record): Promi } : {}), ...(config.tags ? { tags: config.tags } : {}), + ...(landingContentMap[slug] ? { landingContent: landingContentMap[slug] } : {}), }) } }