From 5a7b9242e9a037487ea69b89587ab4d39847aeff Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 14 Sep 2026 15:10:59 -0700 Subject: [PATCH 01/17] feat(audit): record the client surface on audit entries and identify CLI login requests (#7823) --- packages/audit/src/log.test.ts | 42 + packages/audit/src/log.ts | 7 +- .../db/migrations/0344_audit_log_surface.sql | 1 + .../db/migrations/meta/0344_snapshot.json | 26591 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 6 + packages/sim-cli/src/auth/device-flow.test.ts | 4 + packages/sim-cli/src/auth/device-flow.ts | 4 +- packages/sim-cli/src/auth/oauth-flow.test.ts | 6 +- packages/sim-cli/src/auth/oauth-flow.ts | 6 +- packages/sim-cli/src/http/client.ts | 7 +- packages/sim-cli/src/telemetry/client-info.ts | 14 +- packages/sim-cli/src/telemetry/index.ts | 1 - 13 files changed, 26681 insertions(+), 15 deletions(-) create mode 100644 packages/db/migrations/0344_audit_log_surface.sql create mode 100644 packages/db/migrations/meta/0344_snapshot.json diff --git a/packages/audit/src/log.test.ts b/packages/audit/src/log.test.ts index 2b7ace185f9..d683f199d64 100644 --- a/packages/audit/src/log.test.ts +++ b/packages/audit/src/log.test.ts @@ -15,6 +15,10 @@ vi.mock('drizzle-orm', () => ({ or: vi.fn(), sql: vi.fn(), })) +const { mockGetRequestContext } = vi.hoisted(() => ({ + mockGetRequestContext: vi.fn(), +})) + vi.mock('@sim/logger', () => ({ createLogger: () => ({ info: vi.fn(), @@ -22,6 +26,7 @@ vi.mock('@sim/logger', () => ({ error: vi.fn(), debug: vi.fn(), }), + getRequestContext: mockGetRequestContext, })) vi.mock('@sim/utils/id', () => ({ generateId: () => 'test-uuid-123', @@ -181,6 +186,43 @@ describe('recordAudit', () => { ) }) + it('records the surface the request came from', async () => { + mockGetRequestContext.mockReturnValueOnce({ + requestId: 'req-1', + client: { surface: 'cli', version: '2.1.16', source: 'header' }, + }) + + recordAudit({ + workspaceId: 'ws-1', + actorId: 'user-1', + actorName: 'Test', + actorEmail: 'test@test.com', + action: AuditAction.WORKFLOW_CREATED, + resourceType: AuditResourceType.WORKFLOW, + }) + + await flush() + + expect(dbChainMockFns.values).toHaveBeenCalledWith(expect.objectContaining({ surface: 'cli' })) + }) + + it('records no surface outside a request', async () => { + mockGetRequestContext.mockReturnValueOnce(undefined) + + recordAudit({ + workspaceId: 'ws-1', + actorId: 'user-1', + actorName: 'Test', + actorEmail: 'test@test.com', + action: AuditAction.WORKFLOW_CREATED, + resourceType: AuditResourceType.WORKFLOW, + }) + + await flush() + + expect(dbChainMockFns.values.mock.calls.at(-1)?.[0].surface).toBeUndefined() + }) + it('records null when x-forwarded-for is absent', async () => { const request = new Request('https://example.com', { headers: { 'x-real-ip': '10.0.0.1' }, diff --git a/packages/audit/src/log.ts b/packages/audit/src/log.ts index 69debb5a64b..eb784d9c9b5 100644 --- a/packages/audit/src/log.ts +++ b/packages/audit/src/log.ts @@ -1,5 +1,5 @@ import { auditLog, db, user } from '@sim/db' -import { createLogger } from '@sim/logger' +import { createLogger, getRequestContext } from '@sim/logger' import { createClientIpResolver } from '@sim/security/ip' import { generateShortId } from '@sim/utils/id' import { eq } from 'drizzle-orm' @@ -66,6 +66,10 @@ export function recordAuditBatch(entries: AuditLogParams[]): void { * insert paths so the write shape cannot drift between them. Actor fields * are taken as-is — lazy actor resolution is layered on top by * {@link recordAudit} only. + * + * The surface comes from the ambient request context, which the route wrapper + * resolves once per request, so every entry recorded while serving a request + * is attributed without each caller passing it. It is absent outside a request. */ function buildAuditRow( params: AuditLogParams, @@ -86,6 +90,7 @@ function buildAuditRow( metadata: params.metadata ?? {}, ipAddress: params.request ? clientIpResolver.resolve(params.request.headers) : undefined, userAgent: params.request?.headers.get('user-agent') ?? undefined, + surface: getRequestContext()?.client?.surface, } } diff --git a/packages/db/migrations/0344_audit_log_surface.sql b/packages/db/migrations/0344_audit_log_surface.sql new file mode 100644 index 00000000000..4ca0cc09e2c --- /dev/null +++ b/packages/db/migrations/0344_audit_log_surface.sql @@ -0,0 +1 @@ +ALTER TABLE "audit_log" ADD COLUMN "surface" text; \ No newline at end of file diff --git a/packages/db/migrations/meta/0344_snapshot.json b/packages/db/migrations/meta/0344_snapshot.json new file mode 100644 index 00000000000..62705368fe9 --- /dev/null +++ b/packages/db/migrations/meta/0344_snapshot.json @@ -0,0 +1,26591 @@ +{ + "id": "14877af2-9c71-4562-b75a-7ddfb5e23a24", + "prevId": "3b4f9315-ef45-4b41-81ff-396fb49c2e74", + "version": "7", + "dialect": "postgresql", + "tables": { + "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_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 + }, + "oauth_config": { + "name": "oauth_config", + "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": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_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\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "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 + }, + "surface": { + "name": "surface", + "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.background_work_status": { + "name": "background_work_status", + "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": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_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": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "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": { + "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 + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "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_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 + }, + "organization_id": { + "name": "organization_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 + }, + "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 + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "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": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "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 + }, + "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_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "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" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "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": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS 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": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "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 + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "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": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "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_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "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_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_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_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "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_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by 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_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_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 + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "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.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "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": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_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 + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "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 + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "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 + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "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_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "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_processing_recovery_idx": { + "name": "doc_processing_recovery_idx", + "columns": [ + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "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_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_idx": { + "name": "doc_connector_reconciliation_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"source_seen_at\", '-infinity'::timestamp)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "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_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": 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" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "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 + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "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 + } + }, + "embedding_384_vector_hnsw_idx": { + "name": "embedding_384_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_768_vector_hnsw_idx": { + "name": "embedding_768_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_1024_vector_hnsw_idx": { + "name": "embedding_1024_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_3072_vector_hnsw_idx": { + "name": "embedding_3072_vector_hnsw_idx", + "columns": [ + { + "expression": "(\"embedding_3072\"::halfvec(3072)) halfvec_cosine_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_binary_hnsw_idx": { + "name": "embedding_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding\")::bit(1536)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_384_binary_hnsw_idx": { + "name": "embedding_384_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_384\")::bit(384)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_768_binary_hnsw_idx": { + "name": "embedding_768_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_768\")::bit(768)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_1024_binary_hnsw_idx": { + "name": "embedding_1024_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_1024\")::bit(1024)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_3072_binary_hnsw_idx": { + "name": "embedding_3072_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_3072\")::bit(3072)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": 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_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_search": { + "name": "embedding_search", + "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 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_search_kb_idx": { + "name": "embedding_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_search_binary_hnsw_idx": { + "name": "embedding_search_binary_hnsw_idx", + "columns": [ + { + "expression": "binary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_binary_hnsw_idx": { + "name": "embedding_search_384_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_binary_hnsw_idx": { + "name": "embedding_search_768_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_binary_hnsw_idx": { + "name": "embedding_search_1024_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_binary_hnsw_idx": { + "name": "embedding_search_3072_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + } + }, + "foreignKeys": { + "embedding_search_id_embedding_id_fk": { + "name": "embedding_search_id_embedding_id_fk", + "tableFrom": "embedding_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_search_width_check": { + "name": "embedding_search_width_check", + "value": "num_nonnulls(\"binary\", \"binary_384\", \"binary_768\", \"binary_1024\", \"binary_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "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": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "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": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "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 + }, + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "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": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_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": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "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": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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.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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_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 + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_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": {} + }, + "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_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_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": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"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": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "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 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_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()" + }, + "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": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS 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" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_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": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_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 + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "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_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "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.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_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": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "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": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "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": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "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_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "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 + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "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": {}, + "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 + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_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": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_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": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "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": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_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": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_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": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "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 + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_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": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_invocation": { + "name": "organization_search_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_types": { + "name": "source_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_invocation_org_created_idx": { + "name": "organization_search_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_invocation_user_idx": { + "name": "organization_search_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_invocation_organization_id_organization_id_fk": { + "name": "organization_search_invocation_organization_id_organization_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_invocation_user_id_user_id_fk": { + "name": "organization_search_invocation_user_id_user_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_invocation_result_count_bounds": { + "name": "organization_search_invocation_result_count_bounds", + "value": "\"organization_search_invocation\".\"result_count\" BETWEEN 0 AND 100" + }, + "organization_search_invocation_source_types_bounds": { + "name": "organization_search_invocation_source_types_bounds", + "value": "cardinality(\"organization_search_invocation\".\"source_types\") <= 100" + } + }, + "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_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_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": "\"outbox_event\".\"status\" = 'pending'", + "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": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_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 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_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": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "oauth_config": { + "name": "oauth_config", + "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_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_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": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "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 + }, + "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()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "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_organization_name_unique": { + "name": "permission_group_organization_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": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_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 + }, + "organization_id": { + "name": "organization_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_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_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.permission_group_workspace": { + "name": "permission_group_workspace", + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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_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.pinned_item": { + "name": "pinned_item", + "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 + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_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": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_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": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_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": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "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.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "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": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_group_idx": { + "name": "scim_group_mapping_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "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": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "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": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_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": {} + } + }, + "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 + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "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.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "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": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_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": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "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'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "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": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "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 + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_org_domain_unique": { + "name": "sso_provider_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(regexp_replace(btrim(\"domain\"), '^\\*\\.', ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"sso_provider\".\"organization_id\" is not null", + "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 + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "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": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "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_jobs": { + "name": "table_jobs", + "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 + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_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": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "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" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "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" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "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": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "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 + }, + "knowledge_base_id": { + "name": "knowledge_base_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 + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_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": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "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 + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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_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 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "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 + }, + "folder_id": { + "name": "folder_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 + }, + "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 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_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_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "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 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": 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": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "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 + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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": {} + }, + "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_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "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": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "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": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "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 + }, + "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 + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "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_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "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": "\"workflow\".\"archived_at\" IS NULL", + "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_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "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 + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": 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_operation": { + "name": "workflow_deployment_operation", + "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": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "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 + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": 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": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "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_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": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "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 + }, + "deployment_operation_id": { + "name": "deployment_operation_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 + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_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": { + "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_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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 + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "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": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "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" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_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_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_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_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_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "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 + }, + "organization_id": { + "name": "organization_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 + }, + "message_id": { + "name": "message_id", + "type": "text", + "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, + "default": 0 + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "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_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "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": { + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "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_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "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_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "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()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "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()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "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 + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "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_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_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": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "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.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "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_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "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.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "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.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "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", + "voice-output", + "api-tool" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "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 2ad142e4bdb..d97e85619f9 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2402,6 +2402,13 @@ "when": 1789410616549, "tag": "0343_stored_embedding_candidates", "breakpoints": true + }, + { + "idx": 344, + "version": "7", + "when": 1789423183443, + "tag": "0344_audit_log_surface", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 1db1f5fde3e..4f8ea8a7486 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4570,6 +4570,12 @@ export const auditLog = pgTable( metadata: jsonb('metadata').default('{}'), ipAddress: text('ip_address'), userAgent: text('user_agent'), + /** + * The official client the request came from (`web`, `desktop`, `cli`, + * `sdk-js`, `sdk-python`), as resolved from `X-Sim-Client-Info`. Null for + * background work and callers that do not identify themselves. + */ + surface: text('surface'), createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ diff --git a/packages/sim-cli/src/auth/device-flow.test.ts b/packages/sim-cli/src/auth/device-flow.test.ts index 5eea4972c44..7b03fb7c064 100644 --- a/packages/sim-cli/src/auth/device-flow.test.ts +++ b/packages/sim-cli/src/auth/device-flow.test.ts @@ -204,6 +204,10 @@ describe('createAuthRequest', () => { await pollForKey(prefixed, auth) expect(fetchSpy.mock.calls[0][0]).toBe('https://host.test/sim/api/cli/auth/poll') + expect(fetchSpy.mock.calls[0][1]?.headers).toMatchObject({ + 'user-agent': expect.stringMatching(/^sim-cli\//), + 'x-sim-client-info': expect.stringMatching(/^cli\//), + }) }) it('omits an absent workspace rather than sending it blank', () => { diff --git a/packages/sim-cli/src/auth/device-flow.ts b/packages/sim-cli/src/auth/device-flow.ts index 5e319905a67..b5d60619480 100644 --- a/packages/sim-cli/src/auth/device-flow.ts +++ b/packages/sim-cli/src/auth/device-flow.ts @@ -1,7 +1,7 @@ import { createHash, randomBytes, randomInt } from 'node:crypto' import { sleep } from '../helpers' import { buildUrl, REDIRECT_STATUSES, redirectEndpoint, SimApiError } from '../http/client' -import { USER_AGENT } from '../version' +import { identityHeaders } from '../telemetry/client-info' /** * The terminal half of the CLI key handoff. @@ -192,7 +192,7 @@ export async function pollForKey( headers: { 'content-type': 'application/json', accept: 'application/json', - 'user-agent': USER_AGENT, + ...identityHeaders(), }, body: JSON.stringify({ request: auth.request, verifier: auth.pollSecret }), signal, diff --git a/packages/sim-cli/src/auth/oauth-flow.test.ts b/packages/sim-cli/src/auth/oauth-flow.test.ts index 8322ab06464..2e6e8b30695 100644 --- a/packages/sim-cli/src/auth/oauth-flow.test.ts +++ b/packages/sim-cli/src/auth/oauth-flow.test.ts @@ -143,7 +143,11 @@ describe('token endpoint', () => { const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(url).toBe(`${ENDPOINT}/api/auth/oauth2/token`) - expect(init.headers).toMatchObject({ 'content-type': 'application/x-www-form-urlencoded' }) + expect(init.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded', + 'user-agent': expect.stringMatching(/^sim-cli\//), + 'x-sim-client-info': expect.stringMatching(/^cli\//), + }) expect(init.redirect).toBe('manual') expect(Object.fromEntries(new URLSearchParams(String(init.body)))).toEqual({ grant_type: 'authorization_code', diff --git a/packages/sim-cli/src/auth/oauth-flow.ts b/packages/sim-cli/src/auth/oauth-flow.ts index 391081631be..fe6a26e9d64 100644 --- a/packages/sim-cli/src/auth/oauth-flow.ts +++ b/packages/sim-cli/src/auth/oauth-flow.ts @@ -3,7 +3,7 @@ import { createServer, type Server } from 'node:http' import type { AddressInfo } from 'node:net' import { oauthIssuerForEndpoint, redact } from '../config/profile' import { buildUrl, REDIRECT_STATUSES, SimApiError } from '../http/client' -import { USER_AGENT } from '../version' +import { identityHeaders } from '../telemetry/client-info' /** * The OAuth half of `sim login`: authorization code + PKCE with a loopback @@ -183,7 +183,7 @@ export async function discoverOAuthProvider(endpoint: string): Promise { + return { 'user-agent': USER_AGENT, [CLIENT_INFO_HEADER]: clientInfoHeader() } +} + function buildClientInfoHeader(env: NodeJS.ProcessEnv): string { return formatClientInfo({ surface: 'cli', diff --git a/packages/sim-cli/src/telemetry/index.ts b/packages/sim-cli/src/telemetry/index.ts index 8fffcc31156..34e6298fed7 100644 --- a/packages/sim-cli/src/telemetry/index.ts +++ b/packages/sim-cli/src/telemetry/index.ts @@ -1,4 +1,3 @@ -export { clientInfoHeader } from './client-info' export { createCommandTelemetry } from './invocation' export { DO_NOT_TRACK_VARIABLE, From ad05dae73ead24f8c6296a933f0e7523da218df6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 14 Sep 2026 15:11:55 -0700 Subject: [PATCH 02/17] fix(agent): retain conversation memory attachments (#7818) --- .github/workflows/test-build.yml | 4 +- .../content/docs/academy/agents/memory.mdx | 2 + apps/sim/executor/constants.ts | 1 + .../handlers/agent/agent-handler.test.ts | 166 ++++++ .../executor/handlers/agent/agent-handler.ts | 90 ++- .../agent/memory-harness.postgres.test.ts | 547 ++++++++++++++++++ .../executor/handlers/agent/memory.test.ts | 185 +++++- apps/sim/executor/handlers/agent/memory.ts | 65 ++- apps/sim/executor/handlers/agent/types.ts | 6 + .../sim/lib/memory/message-provenance.test.ts | 100 +++- 10 files changed, 1099 insertions(+), 67 deletions(-) create mode 100644 apps/sim/executor/handlers/agent/memory-harness.postgres.test.ts diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 6ad195ff86d..ab28f35e8a5 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -170,15 +170,17 @@ jobs: if-no-files-found: ignore retention-days: 7 - - name: Verify durable provenance bindings and concurrent memory writes + - name: Verify durable provenance, concurrent memory writes, and attachment replay working-directory: apps/sim env: TABLE_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim MEMORY_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim + AGENT_MEMORY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim run: >- bunx vitest run lib/table/rows/secret-provenance.postgres.test.ts lib/memory/message-provenance.postgres.test.ts + executor/handlers/agent/memory-harness.postgres.test.ts - name: Verify Search progress, pagination, and outbox scheduling in PostgreSQL working-directory: apps/sim diff --git a/apps/docs/content/docs/academy/agents/memory.mdx b/apps/docs/content/docs/academy/agents/memory.mdx index 483a70c404d..48a11d441d7 100644 --- a/apps/docs/content/docs/academy/agents/memory.mdx +++ b/apps/docs/content/docs/academy/agents/memory.mdx @@ -18,6 +18,8 @@ import { AV_MEMORY_WORKFLOW } from '@/components/workflow-preview/academy-video- By default, an agent keeps nothing between runs: every conversation starts completely fresh. The Memory setting changes that: choose Conversation, give it a conversation ID, and everything said under that key is kept and loaded back before the model runs. +Uploaded attachments stay linked to the message that included them. Memory stores file references; each later run reads the accessible files again and prepares them for the selected provider. Attachments follow the selected memory window and the source file's storage retention. A replay can include up to 20 attachment references; use a smaller memory window for longer file-heavy conversations. Files omitted by older versions of memory need to be attached again. + { }) }) + describe('conversation attachment replay', () => { + beforeEach(() => { + dbChainMockFns.returning.mockResolvedValue([{ id: 'memory-1' }]) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + const file = { + id: 'file-1', + name: 'example.png', + key: 'execution/test-workspace/test-workflow/exec-1/example.png', + url: 'https://storage.example.com/expired', + size: 8, + type: 'image/png', + context: 'execution', + base64: 'iVBORw0KGgo=', + } + + it.each(['files', 'messages', 'userPrompt'] as const)( + 'replays a previous turn from %s with a fresh provider attachment', + async (source) => { + mockGetProviderFromModel.mockReturnValue('openai') + const hydrate = vi + .spyOn(userFileBase64, 'hydrateUserFilesWithBase64') + .mockImplementation(async (value) => { + const files = value as (typeof file)[] + return files.map((attachment) => ({ + ...attachment, + base64: file.base64, + })) as typeof value + }) + const inputs: AgentInputs = { + model: 'gpt-4o', + memoryType: 'conversation', + conversationId: 'conversation-1', + ...(source === 'userPrompt' + ? { userPrompt: 'Analyze this file', files: [file] } + : { + messages: [ + { + role: 'user', + content: 'Analyze this file', + ...(source === 'messages' ? { files: [file] } : {}), + }, + ], + ...(source === 'files' ? { files: [file] } : {}), + }), + } + const original = structuredClone(inputs) + await handler.execute({ ...mockContext, executionId: 'exec-1' }, mockBlock, inputs) + const stored = dbChainMockFns.values.mock.calls + .map(([row]) => row) + .find((row) => Array.isArray(row.data) && row.data[0]?.role === 'user')?.data as Message[] + expect(stored).toBeDefined() + expect(stored[0].files).toEqual([ + { + id: file.id, + name: file.name, + key: file.key, + url: '', + size: file.size, + type: file.type, + context: file.context, + }, + ]) + expect(inputs).toEqual(original) + + queueTableRows(schemaMock.memory, [ + { data: [...stored, { role: 'assistant', content: 'First answer' }] }, + ]) + mockGetProviderFromModel.mockReturnValue('anthropic') + const nextContext = { ...mockContext, executionId: 'exec-2' } + await handler.execute(nextContext, mockBlock, { + model: 'claude-sonnet-4-5', + memoryType: 'conversation', + conversationId: 'conversation-1', + messages: [{ role: 'user', content: 'What is in that file?' }], + }) + const request = mockExecuteProviderRequest.mock.calls.at(-1)?.[1] + expect(request.messages[0]).toMatchObject({ + role: 'user', + content: 'Analyze this file', + files: [{ key: file.key, base64: file.base64 }], + }) + expect(request.messages.at(-1)).toMatchObject({ + role: 'user', + content: 'What is in that file?', + }) + expect(request.messages.at(-1).files).toBeUndefined() + expect(hydrate.mock.calls.at(-1)?.[0]).toEqual(stored[0].files) + expect(hydrate.mock.calls.at(-1)?.[1]).toMatchObject({ + executionId: 'exec-2', + fileKeys: [file.key], + }) + hydrate.mockRestore() + } + ) + + it('does not duplicate an attachment when the same execution revisits the agent', async () => { + mockGetProviderFromModel.mockReturnValue('openai') + queueTableRows(schemaMock.memory, [ + { + data: [ + { role: 'user', content: 'Analyze this file', executionId: 'exec-1', files: [file] }, + ], + }, + ]) + await handler.execute({ ...mockContext, executionId: 'exec-1' }, mockBlock, { + model: 'gpt-4o', + memoryType: 'conversation', + conversationId: 'conversation-1', + messages: [{ role: 'user', content: 'Analyze this file' }], + files: [file], + }) + expect(mockExecuteProviderRequest.mock.calls[0][1].messages[0].files).toHaveLength(1) + expect(dbChainMockFns.values.mock.calls.some(([row]) => row.data?.[0]?.role === 'user')).toBe( + false + ) + }) + + it('saves a new attachment appended to an existing conversation', async () => { + mockGetProviderFromModel.mockReturnValue('openai') + queueTableRows(schemaMock.memory, [{ data: [{ role: 'assistant', content: 'Hello' }] }]) + await handler.execute({ ...mockContext, executionId: 'exec-2' }, mockBlock, { + model: 'gpt-4o', + memoryType: 'conversation', + conversationId: 'conversation-1', + messages: [{ role: 'user', content: 'Analyze this file' }], + files: [file], + }) + const stored = dbChainMockFns.values.mock.calls + .map(([row]) => row) + .find((row) => row.data?.[0]?.role === 'user')?.data as Message[] + expect(stored[0].files?.[0]).toMatchObject({ key: file.key, url: '' }) + expect(stored[0].files?.[0].base64).toBeUndefined() + }) + + it('does not hydrate attachments excluded by the conversation window', async () => { + mockGetProviderFromModel.mockReturnValue('openai') + const hydrate = vi.spyOn(userFileBase64, 'hydrateUserFilesWithBase64') + queueTableRows(schemaMock.memory, [ + { + data: [ + { role: 'user', content: 'Old file', files: [file] }, + { role: 'assistant', content: 'Recent answer' }, + ], + }, + ]) + const context = { ...mockContext, executionId: 'exec-2' } + await handler.execute(context, mockBlock, { + model: 'gpt-4o', + memoryType: 'sliding_window', + slidingWindowSize: '1', + conversationId: 'conversation-1', + messages: [{ role: 'user', content: 'Hello' }], + }) + expect(hydrate).not.toHaveBeenCalled() + expect(context.fileKeys).toBeUndefined() + hydrate.mockRestore() + }) + }) + describe('execute', () => { it('should execute a basic agent block request', async () => { const inputs = { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index a78105e8aba..0ccc9c52f80 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -53,6 +53,7 @@ import { } from '@/executor/handlers/agent/skills-resolver' import type { AgentInputs, + FileNameProjection, Message, StreamingConfig, ToolInput, @@ -374,14 +375,12 @@ export class AgentBlockHandler implements BlockHandler { } const streamingConfig = this.getStreamingConfig(ctx, block) - const messages = await this.buildMessages(ctx, filteredInputs, modelInputs, skillMetadata) - const messagesWithInputFiles = this.attachFilesToLastUserMessage( + const messagesWithInputFiles = await this.buildMessages( ctx, - messages, - filteredInputs.files, - fileProjection.projectedFiles, - fileProjection.projectedNameByFile, - fileProjection.directNameInputPaths + filteredInputs, + modelInputs, + skillMetadata, + fileProjection ) const messagesWithFiles = await this.hydrateMessageFilesForProvider( ctx, @@ -1212,10 +1211,13 @@ export class AgentBlockHandler implements BlockHandler { ctx: ExecutionContext, inputs: AgentInputs, modelInputs: AgentInputs, - skillMetadata: Array<{ name: string; description: string }> = [] + skillMetadata: Array<{ name: string; description: string }>, + fileProjection: ReturnType ): Promise { const messages: Message[] = [] const memoryEnabled = inputs.memoryType && inputs.memoryType !== 'none' + const pendingMemoryMessages: Array<{ raw: Message; model: Message }> = [] + let seedMessageCount = 0 // 1. Extract and validate messages from messages-input subblock const inputMessages = this.extractValidMessages(inputs.messages) @@ -1226,7 +1228,11 @@ export class AgentBlockHandler implements BlockHandler { // 2. Handle native memory: seed on first run, then fetch and append new user input if (memoryEnabled && ctx.workspaceId) { - const memoryMessages = await memoryService.fetchMemoryMessages(ctx, inputs) + const memoryMessages = await memoryService.fetchMemoryMessages( + ctx, + inputs, + fileProjection.projectedNameByFile + ) const hasExisting = memoryMessages.length > 0 if (!hasExisting && conversationMessages.length > 0) { @@ -1236,7 +1242,13 @@ export class AgentBlockHandler implements BlockHandler { const rawTaggedMessages = rawConversationMessages.map((m) => m.role === 'user' ? { ...m, executionId: ctx.executionId } : m ) - await memoryService.seedMemory(ctx, inputs, rawTaggedMessages) + for (let index = 0; index < taggedMessages.length; index++) { + pendingMemoryMessages.push({ + raw: rawTaggedMessages[index], + model: taggedMessages[index], + }) + } + seedMessageCount = taggedMessages.length messages.push(...taggedMessages) } else { messages.push(...memoryMessages) @@ -1261,9 +1273,9 @@ export class AgentBlockHandler implements BlockHandler { if (!userMessageInThisRun) { const taggedMessage = { ...latestUserFromInput, executionId: ctx.executionId } messages.push(taggedMessage) - await memoryService.appendToMemory(ctx, inputs, { - ...latestRawUserFromInput, - executionId: ctx.executionId, + pendingMemoryMessages.push({ + raw: { ...latestRawUserFromInput, executionId: ctx.executionId }, + model: taggedMessage, }) } } @@ -1300,9 +1312,9 @@ export class AgentBlockHandler implements BlockHandler { const userMessages = messages.filter((m) => m.role === 'user') const lastUserMessage = userMessages[userMessages.length - 1] if (lastUserMessage) { - await memoryService.appendToMemory(ctx, inputs, { - ...lastUserMessage, - content: this.formatUserPrompt(inputs.userPrompt), + pendingMemoryMessages.push({ + raw: { ...lastUserMessage, content: this.formatUserPrompt(inputs.userPrompt) }, + model: lastUserMessage, }) } } @@ -1328,7 +1340,33 @@ export class AgentBlockHandler implements BlockHandler { } } - return messages.length > 0 ? messages : undefined + const messagesWithFiles = this.attachFilesToLastUserMessage( + ctx, + messages.length > 0 ? messages : undefined, + inputs.files, + fileProjection.projectedFiles, + fileProjection.projectedNameByFile, + fileProjection.directNameInputPaths + ) + + /** Persist the complete turn before provider hydration adds bytes or transient handles. */ + const lastUserMessage = messages.filter((message) => message.role === 'user').at(-1) + const attachedUserMessage = messagesWithFiles + ?.filter((message) => message.role === 'user') + .at(-1) + const messagesToStore = pendingMemoryMessages.map(({ raw, model }) => + model === lastUserMessage && attachedUserMessage?.files + ? { ...raw, files: attachedUserMessage.files } + : raw + ) + if (seedMessageCount > 0) { + await memoryService.seedMemory(ctx, inputs, messagesToStore.slice(0, seedMessageCount)) + } + for (const message of messagesToStore.slice(seedMessageCount)) { + await memoryService.appendToMemory(ctx, inputs, message) + } + + return messagesWithFiles } private attachFilesToLastUserMessage( @@ -1336,7 +1374,7 @@ export class AgentBlockHandler implements BlockHandler { messages: Message[] | undefined, filesInput: unknown, projectedFilesInput: unknown, - projectedNameByFile: WeakMap, + projectedNameByFile: WeakMap, directNameInputPaths: readonly ResolvedSecretInputPath[] ): Message[] | undefined { const normalizedFiles = normalizeFileInput(filesInput) @@ -1397,10 +1435,13 @@ export class AgentBlockHandler implements BlockHandler { } const lastUserMessage = messages[lastUserMessageIndex] + const filesByKey = new Map( + [...(lastUserMessage.files ?? []), ...userFiles].map((file) => [file.key || file.id, file]) + ) const nextMessages = [...messages] nextMessages[lastUserMessageIndex] = { ...lastUserMessage, - files: [...(lastUserMessage.files ?? []), ...userFiles], + files: Array.from(filesByKey.values()), } return nextMessages @@ -1410,7 +1451,7 @@ export class AgentBlockHandler implements BlockHandler { ctx: ExecutionContext, messages: Message[] | undefined, providerId: string, - projectedNameByFile: WeakMap, + projectedNameByFile: WeakMap, modelBoundInputPaths: ResolvedSecretInputPath[] ): Promise { if (!messages?.some((message) => message.files?.length)) { @@ -1478,7 +1519,7 @@ export class AgentBlockHandler implements BlockHandler { return [file] } - modelBoundInputPaths.push(nameProjection.inputPath) + if (nameProjection.inputPath) modelBoundInputPaths.push(nameProjection.inputPath) const extension = getFileExtension(file.name) const suffix = extension ? `.${extension}` : '' const keepsSuffix = @@ -2004,7 +2045,7 @@ export class AgentBlockHandler implements BlockHandler { inputs: AgentInputs ): { projectedFiles: unknown - projectedNameByFile: WeakMap + projectedNameByFile: WeakMap directNameInputPaths: ResolvedSecretInputPath[] modelBoundInputPaths: ResolvedSecretInputPath[] } { @@ -2104,10 +2145,7 @@ export class AgentBlockHandler implements BlockHandler { } } - const projectedNameByFile = new WeakMap< - object, - { name: string; inputPath: ResolvedSecretInputPath } - >() + const projectedNameByFile = new WeakMap() const projectedMessages = Array.isArray(projection.value.messages) ? projection.value.messages : [] diff --git a/apps/sim/executor/handlers/agent/memory-harness.postgres.test.ts b/apps/sim/executor/handlers/agent/memory-harness.postgres.test.ts new file mode 100644 index 00000000000..7c9ebf1a3f5 --- /dev/null +++ b/apps/sim/executor/handlers/agent/memory-harness.postgres.test.ts @@ -0,0 +1,547 @@ +/** + * @vitest-environment node + * + * Opt-in harness: AGENT_MEMORY_TEST_DATABASE_URL must point to disposable local PostgreSQL. + * Run from apps/sim with `bun run test executor/handlers/agent/memory-harness.postgres.test.ts`. + * AGENT_MEMORY_TEST_LIVE=1 uses configured OpenAI/Anthropic credentials and synthetic PDFs. + * Otherwise only provider HTTP responses are simulated; storage, SQL, hydration, dispatch, + * SDK request construction, response parsing, and memory persistence execute real code. + * Account policy/key lookup use fixtures, Redis uses the in-memory fallback, and no API route runs. + * AGENT_MEMORY_TEST_REPORT optionally writes SQL snapshots and outgoing attachment hashes. + */ +import { createHash } from 'node:crypto' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { generateId, generateShortId } from '@sim/utils/id' +import { sql } from 'drizzle-orm' +import { getTableConfig, PgDialect, type PgTable } from 'drizzle-orm/pg-core' +import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js' +import { PDFDocument, StandardFonts } from 'pdf-lib' +import postgres from 'postgres' +import { fetch as networkFetch } from 'undici' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +const fixture = vi.hoisted(() => ({ + database: undefined as PostgresJsDatabase | undefined, + uploads: '', +})) + +vi.unmock('drizzle-orm') +vi.unmock('@sim/db/schema') +vi.mock('@sim/db', () => ({ + db: new Proxy( + {}, + { + get(_target, property) { + if (!fixture.database) throw new Error('Harness database is not initialized') + const value = Reflect.get(fixture.database, property) + return typeof value === 'function' ? value.bind(fixture.database) : value + }, + } + ), +})) +vi.mock('@/lib/uploads/core/setup.server', () => ({ + get UPLOAD_DIR_SERVER() { + return fixture.uploads + }, +})) +/** Fixture principals and explicit keys replace account configuration, not file authorization. */ +vi.mock('@/ee/access-control/utils/permission-check', () => ({ + assertPermissionsAllowed: async () => {}, + validateModelProvider: async () => {}, + validateBlockType: async () => {}, +})) +vi.mock('@/lib/api-key/byok', () => ({ + getApiKeyWithBYOK: async ( + _provider: string, + _model: string, + _workspace: string, + apiKey: string + ) => ({ apiKey, isBYOK: true }), +})) + +import { + memory, + memorySecretProvenance, + workspaceFileSecretProvenance, + workspaceFiles, +} from '@sim/db/schema' +import { hashDurableSecretProvenanceValue } from '@/lib/execution/durable-secret-provenance' +import { uploadExecutionFile } from '@/lib/uploads/contexts/execution/execution-file-manager' +import { EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' +import { deleteFile } from '@/lib/uploads/core/storage-service' +import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' +import type { AgentInputs, Message } from '@/executor/handlers/agent/types' +import type { ExecutionContext, StreamingExecution, UserFile } from '@/executor/types' +import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { createAgentStreamPump } from '@/providers/stream-pump' +import type { SerializedBlock } from '@/serializer/types' + +const databaseUrl = process.env.AGENT_MEMORY_TEST_DATABASE_URL +if (databaseUrl && !['localhost', '127.0.0.1', '[::1]'].includes(new URL(databaseUrl).hostname)) { + throw new Error('The Agent memory harness requires a disposable local database') +} +const live = process.env.AGENT_MEMORY_TEST_LIVE === '1' +const schemaName = `agent_memory_${generateId().replaceAll('-', '')}` +const connection = databaseUrl + ? postgres(databaseUrl, { + max: 3, + connection: { search_path: schemaName }, + onnotice: () => {}, + }) + : undefined +const scope = { workspaceId: generateId(), workflowId: generateId(), userId: generateId() } +const block = { + id: generateId(), + metadata: { id: 'agent', name: 'Memory harness' }, + position: { x: 0, y: 0 }, + config: { tool: '', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, +} as SerializedBlock +const models = { + openai: process.env.AGENT_MEMORY_TEST_OPENAI_MODEL || 'gpt-4.1-mini', + anthropic: process.env.AGENT_MEMORY_TEST_ANTHROPIC_MODEL || 'claude-haiku-4-5', +} as const +type Provider = keyof typeof models +interface WireRequest { + host: string + body: Record +} +interface StoredConversation { + data: Message[] + secret_provenance_version: number + content_hash: string + status: string + entries: unknown[] +} +const report: Array> = [] +let outbound: WireRequest[] = [] +let transportReply = 'READY' + +function apiKey(provider: Provider): string { + if (!live) return 'synthetic-harness-key' + const value = + provider === 'openai' + ? process.env.OPENAI_API_KEY + : process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY_1 + if (!value) throw new Error(`Live harness requires a configured ${provider} API key`) + return value +} + +function context(streaming = false): ExecutionContext { + return { + ...scope, + executionId: generateId(), + principal: { + kind: 'system', + serviceId: 'schedule', + workspaceId: scope.workspaceId, + workflowId: scope.workflowId, + }, + stream: streaming, + selectedOutputs: [block.id], + blockStates: new Map(), + blockLogs: [], + metadata: { startTime: new Date().toISOString(), duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopExecutions: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + workflow: { blocks: [], connections: [], loops: {}, version: '1' }, + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry([], scope), + } as ExecutionContext +} + +/** Use the executor's stream pump and completion hook to persist streamed assistant turns. */ +async function executeTurn(ctx: ExecutionContext, inputs: AgentInputs): Promise { + const result = await new AgentBlockHandler().execute(ctx, block, inputs) + if (!ctx.stream) return String(result.content) + expect(result).toHaveProperty('stream') + const streamingResult = result as StreamingExecution + expect(streamingResult.onFullContent).toBeTypeOf('function') + const pump = createAgentStreamPump({ + source: streamingResult.stream, + streamFormat: streamingResult.streamFormat ?? 'text', + sinkMode: true, + }) + const drained = await pump.run() + expect(drained.fullyDrained).toBe(true) + expect(drained.cancelled).toBe(false) + await streamingResult.onFullContent!(drained.answerText) + return drained.answerText +} + +/** Generate only the four production tables exercised here; unrelated application FKs are omitted. */ +async function createTable(table: PgTable): Promise { + if (!connection) throw new Error('Missing harness database') + const dialect = new PgDialect() + const config = getTableConfig(table) + const columns = config.columns.map((column) => { + const defaultValue = + column.dataType === 'json' && column.default !== undefined + ? sql`${JSON.stringify(column.default)}::jsonb` + : sql`${column.default}` + const defaultSql = + column.default === undefined + ? '' + : ` DEFAULT ${dialect.sqlToQuery(defaultValue.inlineParams()).sql}` + return `"${column.name}" ${column.getSQLType()}${column.primary ? ' PRIMARY KEY' : ''}${column.notNull ? ' NOT NULL' : ''}${defaultSql}` + }) + await connection.unsafe(`CREATE TABLE "${config.name}" (${columns.join(', ')})`) +} + +async function readConversation(key: string): Promise { + if (!connection) throw new Error('Missing harness database') + const [row] = await connection` + SELECT m.data, m.secret_provenance_version, p.content_hash, p.status, p.entries + FROM memory m LEFT JOIN memory_secret_provenance p ON p.memory_id = m.id + WHERE m.workspace_id = ${scope.workspaceId} AND m.key = ${key}` + if (!row) throw new Error('Conversation was not persisted') + expect(row.secret_provenance_version).toBe(1) + expect(row.status).toBe('exact') + expect(row.content_hash).toBe(hashDurableSecretProvenanceValue(row.data)) + expect(row.entries).toEqual([]) + return row +} + +function requestFiles(request: WireRequest): string[] { + const messages = request.host === 'api.openai.com' ? request.body.input : request.body.messages + if (!Array.isArray(messages)) throw new Error('Provider did not send a message array') + return messages.flatMap((message) => { + if (!Array.isArray(message.content)) return [] + return message.content.flatMap((part: Record) => { + if (part.type === 'input_file' && typeof part.file_data === 'string') + return [part.file_data.split(',')[1]] + if ( + part.type === 'document' && + part.source && + typeof part.source === 'object' && + 'data' in part.source + ) { + return [String(part.source.data)] + } + return [] + }) + }) +} + +async function interceptFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const request = new Request(input, init) + const url = new URL(request.url) + if (!['api.openai.com', 'api.anthropic.com'].includes(url.hostname)) { + throw new Error(`Unexpected harness network destination: ${url.hostname}`) + } + const text = await request.text() + const body = JSON.parse(text) as Record + outbound.push({ host: url.hostname, body }) + if (live) { + const response = await networkFetch(url, { + method: request.method, + headers: Object.fromEntries(request.headers), + body: text, + signal: AbortSignal.timeout(60_000), + }) + return new Response(await response.arrayBuffer(), { + status: response.status, + headers: Object.fromEntries(response.headers), + }) + } + const content = transportReply + const response = + url.hostname === 'api.openai.com' + ? { + id: 'resp_harness', + object: 'response', + status: 'completed', + model: body.model, + output: [ + { + id: 'msg_harness', + type: 'message', + role: 'assistant', + status: 'completed', + content: [{ type: 'output_text', text: content, annotations: [] }], + }, + ], + usage: { input_tokens: 100, output_tokens: 8, total_tokens: 108 }, + } + : { + id: 'msg_harness', + type: 'message', + role: 'assistant', + model: body.model, + content: [{ type: 'text', text: content }], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 100, output_tokens: 8 }, + } + if (!body.stream) return Response.json(response) + const events = + url.hostname === 'api.openai.com' + ? [ + { type: 'response.output_text.delta', delta: content }, + { type: 'response.completed', response }, + ] + : [ + { type: 'message_start', message: { ...response, content: [] } }, + { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: content } }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'end_turn', stop_sequence: null }, + usage: { output_tokens: 8 }, + }, + { type: 'message_stop' }, + ] + return new Response( + events.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(''), + { headers: { 'Content-Type': 'text/event-stream' } } + ) +} + +describe.skipIf(!databaseUrl)( + 'Agent memory through PostgreSQL, storage, and provider transports', + () => { + beforeAll(async () => { + if (!connection) throw new Error('Missing harness database') + fixture.uploads = await mkdtemp(join(tmpdir(), 'sim-memory-files-')) + await connection`CREATE SCHEMA ${connection(schemaName)}` + fixture.database = drizzle(connection) + for (const table of [ + memory, + memorySecretProvenance, + workspaceFiles, + workspaceFileSecretProvenance, + ]) + await createTable(table) + await connection.unsafe( + `CREATE UNIQUE INDEX memory_workspace_key_idx ON memory(workspace_id, key)` + ) + await connection.unsafe( + `CREATE UNIQUE INDEX workspace_files_key_active_unique ON workspace_files(key) WHERE deleted_at IS NULL` + ) + await connection.unsafe(` + CREATE FUNCTION demote_memory() RETURNS trigger LANGUAGE plpgsql AS $body$ + BEGIN NEW.secret_provenance_version := NULL; RETURN NEW; END; $body$; + CREATE TRIGGER memory_demote BEFORE UPDATE OF data ON memory FOR EACH ROW + WHEN(OLD.data IS DISTINCT FROM NEW.data) EXECUTE FUNCTION demote_memory(); + `) + }) + + afterAll(async () => { + try { + if (process.env.AGENT_MEMORY_TEST_REPORT) { + await writeFile( + process.env.AGENT_MEMORY_TEST_REPORT, + JSON.stringify({ live, cases: report }, null, 2) + ) + } + if (connection) await connection`DROP SCHEMA IF EXISTS ${connection(schemaName)} CASCADE` + } finally { + fixture.database = undefined + if (connection) await connection.end() + if (fixture.uploads) await rm(fixture.uploads, { recursive: true, force: true }) + vi.unstubAllGlobals() + } + }) + + it.each( + ( + [ + { first: 'openai', second: 'openai', source: 'files' }, + { first: 'anthropic', second: 'anthropic', source: 'messages' }, + { first: 'openai', second: 'anthropic', source: 'userPrompt' }, + { first: 'anthropic', second: 'openai', source: 'files' }, + ] as const + ).flatMap((scenario) => [false, true].map((streaming) => ({ ...scenario, streaming }))) + )( + '$first → $second, using $source, streaming=$streaming', + async ({ first, second, source, streaming }) => { + vi.stubGlobal('fetch', interceptFetch) + outbound = [] + const conversationId = generateId() + const firstContext = context(streaming) + const marker = `PROBE-${generateShortId()}` + const pdf = await PDFDocument.create() + const font = await pdf.embedFont(StandardFonts.Helvetica) + pdf.addPage().drawText(`memory_probe = ${marker}`, { x: 40, y: 700, size: 18, font }) + const buffer = Buffer.from(await pdf.save()) + const file = await uploadExecutionFile( + { + workspaceId: scope.workspaceId, + workflowId: scope.workflowId, + executionId: firstContext.executionId!, + }, + buffer, + 'memory-probe.pdf', + 'application/pdf', + scope.userId, + EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE + ) + expect(file.key).toContain(firstContext.executionId) + if (!connection) throw new Error('Missing harness database') + /** Production provenance compares JavaScript Dates, so compare versions at millisecond precision. */ + const [storedFile] = await connection` + SELECT f.id, f.key, f.workspace_id, f.context, f.original_name, f.content_type, + f.size_bytes::integer AS size_bytes, f.secret_provenance_version, + p.status, p.entries, + date_trunc('milliseconds', f.content_updated_at) = p.content_updated_at AS provenance_bound + FROM workspace_files f + LEFT JOIN workspace_file_secret_provenance p ON p.file_id = f.id + WHERE f.key = ${file.key!}` + expect(storedFile).toEqual({ + id: file.id, + key: file.key, + workspace_id: scope.workspaceId, + context: 'execution', + original_name: 'memory-probe.pdf', + content_type: 'application/pdf', + size_bytes: buffer.length, + secret_provenance_version: 1, + status: 'exact', + entries: [], + provenance_bound: true, + }) + const firstPrompt = + 'Read the attached PDF. Reply exactly READY. Do not quote or mention anything from the file.' + const firstInputs: AgentInputs = { + model: models[first], + apiKey: apiKey(first), + maxTokens: '128', + memoryType: 'conversation', + conversationId, + ...(source === 'userPrompt' + ? { userPrompt: firstPrompt, files: [file] } + : { + messages: [ + { + role: 'user', + content: firstPrompt, + ...(source === 'messages' ? { files: [file] } : {}), + }, + ], + ...(source === 'files' ? { files: [file] } : {}), + }), + } + transportReply = 'READY' + expect(await executeTurn(firstContext, firstInputs)).toBe('READY') + const firstStored = await readConversation(conversationId) + expect(firstStored.data.map((message) => message.role)).toEqual(['user', 'assistant']) + const reference: UserFile = { + id: file.id, + name: file.name, + key: file.key, + url: '', + size: buffer.length, + type: 'application/pdf', + context: 'execution', + } + expect(firstStored.data[0].files).toEqual([reference]) + expect(JSON.stringify(firstStored)).not.toContain(marker) + expect(JSON.stringify(firstStored)).not.toContain('base64') + expect(JSON.stringify(firstStored)).not.toContain('providerFile') + expect(requestFiles(outbound[0])).toEqual([buffer.toString('base64')]) + + /** Fresh handler, execution, and registry force a DB read and a new execution-scoped byte cache. */ + const secondContext = context(streaming) + transportReply = marker + const followupInputs: AgentInputs = { + model: models[second], + apiKey: apiKey(second), + maxTokens: '128', + memoryType: 'conversation', + conversationId, + messages: [ + { + role: 'user', + content: + 'What is the memory_probe value from the PDF I attached earlier? Reply exactly the value. If no PDF is available, reply NO_FILE.', + }, + ], + } + expect(await executeTurn(secondContext, followupInputs)).toBe(marker) + expect(secondContext.fileKeys).toEqual([file.key]) + expect(outbound).toHaveLength(2) + expect(outbound.every((request) => Boolean(request.body.stream) === streaming)).toBe(true) + expect(requestFiles(outbound[1])).toEqual([buffer.toString('base64')]) + const secondStored = await readConversation(conversationId) + expect(secondStored.data.map((message) => message.role)).toEqual([ + 'user', + 'assistant', + 'user', + 'assistant', + ]) + expect(secondStored.data[0]).toEqual(firstStored.data[0]) + expect(secondStored.data[2].files).toBeUndefined() + expect(secondStored.data[3].content).toBe(marker) + + transportReply = 'NO_FILE' + const unrelatedConversationId = generateId() + const unrelatedContext = context(streaming) + expect( + await executeTurn(unrelatedContext, { + ...followupInputs, + conversationId: unrelatedConversationId, + }) + ).toBe('NO_FILE') + expect(unrelatedContext.fileKeys ?? []).toEqual([]) + expect(outbound).toHaveLength(3) + expect(requestFiles(outbound[2])).toEqual([]) + const unrelatedStored = await readConversation(unrelatedConversationId) + expect(unrelatedStored.data).toHaveLength(2) + expect(unrelatedStored.data.every((message) => !message.files)).toBe(true) + + const otherWorkflow = context(streaming) + otherWorkflow.workflowId = generateId() + otherWorkflow.principal = { + kind: 'system', + serviceId: 'schedule', + workspaceId: scope.workspaceId, + workflowId: otherWorkflow.workflowId, + } + await expect(executeTurn(otherWorkflow, followupInputs)).rejects.toThrow( + 'could not be read' + ) + expect(outbound).toHaveLength(3) + + await deleteFile({ key: file.key!, context: 'execution' }) + await expect(executeTurn(context(streaming), followupInputs)).rejects.toThrow( + 'could not be read' + ) + expect(outbound).toHaveLength(3) + const afterDeletion = await readConversation(conversationId) + expect(afterDeletion.data[0].files).toEqual([reference]) + report.push({ + first, + second, + source, + streaming, + marker, + storedFile, + firstStored, + secondStored, + controls: { + unrelatedConversation: 'passed', + differentWorkflow: 'blocked before HTTP', + missingSource: 'blocked before HTTP', + }, + transports: outbound.map((request) => ({ + host: request.host, + fileCount: requestFiles(request).length, + fileSha256: requestFiles(request).map((base64) => + createHash('sha256').update(Buffer.from(base64, 'base64')).digest('hex') + ), + })), + passed: true, + }) + }, + 150_000 + ) + } +) diff --git a/apps/sim/executor/handlers/agent/memory.test.ts b/apps/sim/executor/handlers/agent/memory.test.ts index 465dec67f15..96f629e62ba 100644 --- a/apps/sim/executor/handlers/agent/memory.test.ts +++ b/apps/sim/executor/handlers/agent/memory.test.ts @@ -1,4 +1,4 @@ -import { loggerMock } from '@sim/testing' +import { loggerMock, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockDecryptSecret, mockRedactObjectStrings, mockIsEnforced, mockReportUnrecorded } = @@ -24,10 +24,21 @@ vi.mock('@/lib/logs/execution/pii-redaction', () => ({ })) import { hashDurableSecretProvenanceValue } from '@/lib/execution/durable-secret-provenance' +import { assertUserFileContentAccess } from '@/lib/execution/payloads/materialization.server' import { MEMORY } from '@/executor/constants' import { Memory } from '@/executor/handlers/agent/memory' import type { Message } from '@/executor/handlers/agent/types' +import type { ExecutionContext, UserFile } from '@/executor/types' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + buildAnthropicMessageContent, + buildBedrockMessageContent, + buildGeminiMessageParts, + buildOpenAICompatibleChatContent, + buildOpenAIMessageContent, + buildOpenRouterMessageContent, + prepareProviderAttachments, +} from '@/providers/attachments' const mockMemoryLogger = vi.mocked(loggerMock.createLogger).mock.results[ vi.mocked(loggerMock.createLogger).mock.calls.findIndex(([name]) => name === 'Memory') @@ -44,6 +55,7 @@ describe('Memory', () => { beforeEach(() => { vi.clearAllMocks() + resetDbChainMock() mockIsEnforced.mockReturnValue(false) mockDecryptSecret.mockImplementation(async (encryptedValue: string) => ({ decrypted: `decrypted:${encryptedValue}`, @@ -214,7 +226,7 @@ describe('Memory', () => { }) describe('sanitizeMessageForStorage', () => { - it('should strip file payloads but preserve tool-call fields before memory persistence', () => { + it('preserves storage references and tool calls without file payloads or provider handles', () => { const message: Message = { role: 'user', content: 'Analyze this file', @@ -228,6 +240,9 @@ describe('Memory', () => { size: 128, type: 'image/png', base64: 'iVBORw0KGgo=', + providerFileId: 'expired-provider-file', + providerFileUri: 'expired-provider-uri', + remoteUrl: 'https://storage.example.com/expired', }, ], tool_calls: [{ id: 'call-1' }], @@ -237,11 +252,177 @@ describe('Memory', () => { role: 'user', content: 'Analyze this file', executionId: 'exec-1', + files: [ + { + id: 'file-1', + key: 'workspace/ws-1/example.png', + name: 'example.png', + url: '', + size: 128, + type: 'image/png', + }, + ], tool_calls: [{ id: 'call-1' }], }) }) }) + describe('provider-independent file references', () => { + const storedFile: UserFile = { + id: 'file-1', + key: 'workspace/workspace-1/image.png', + name: 'image.png', + url: '', + type: 'image/png', + size: 8, + context: 'workspace', + } + const bytes = 'iVBORw0KGgo=' + const renderers: Array<{ + providers: string[] + render: (content: string, files: UserFile[], provider: string) => unknown + }> = [ + { providers: ['openai', 'azure-openai'], render: buildOpenAIMessageContent }, + { providers: ['anthropic', 'azure-anthropic'], render: buildAnthropicMessageContent }, + { providers: ['google', 'vertex'], render: buildGeminiMessageParts }, + { providers: ['bedrock'], render: buildBedrockMessageContent }, + { providers: ['openrouter'], render: buildOpenRouterMessageContent }, + { + providers: [ + 'mistral', + 'groq', + 'fireworks', + 'together', + 'baseten', + 'ollama', + 'ollama-cloud', + 'vllm', + 'litellm', + 'xai', + 'kimi', + ], + render: buildOpenAICompatibleChatContent, + }, + ] + const providers = renderers.flatMap(({ providers, render }) => + providers.map((provider) => ({ provider, render })) + ) + + it.each(providers)( + 'keeps the same attachment wire content for $provider', + async ({ provider, render }) => { + queueTableRows(schemaMock.memory, [ + { + data: [ + { + role: 'user', + content: 'Describe the image', + files: [ + { + ...storedFile, + url: 'https://expired.example/file', + base64: 'stale-bytes', + providerFileId: 'stale-id', + providerFileUri: 'stale-uri', + remoteUrl: 'https://expired.example/provider-file', + }, + ], + }, + ], + }, + ]) + const [message] = await memoryService.fetchMemoryMessages( + { workspaceId: 'workspace-1' } as ExecutionContext, + { memoryType: 'conversation', conversationId: 'conversation-1' } + ) + expect(message.files).toEqual([storedFile]) + const hydrated = message.files!.map((file) => ({ ...file, base64: bytes })) + expect(render(message.content, hydrated, provider)).toEqual( + render('Describe the image', [{ ...storedFile, base64: bytes }], provider) + ) + } + ) + + it.each(['deepseek', 'cerebras', 'sakana', 'nvidia', 'meta', 'zai'])( + 'keeps the explicit unsupported-attachment error for %s', + (provider) => { + expect(() => + prepareProviderAttachments([{ ...storedFile, base64: bytes }], provider) + ).toThrow('File attachments are not supported') + } + ) + + it('admits only the remembered execution file and preserves workspace and workflow scope', async () => { + const file = { + ...storedFile, + key: 'execution/workspace-1/workflow-1/exec-1/image.png', + context: 'execution', + } + queueTableRows(schemaMock.memory, [ + { data: [{ role: 'user', content: 'File', files: [file] }] }, + ]) + const context = { + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + executionId: 'exec-2', + } as ExecutionContext + await memoryService.fetchMemoryMessages(context, { + memoryType: 'conversation', + conversationId: 'conversation-1', + }) + await expect(assertUserFileContentAccess(file, context)).resolves.toBeUndefined() + await expect( + assertUserFileContentAccess( + { ...file, key: 'execution/workspace-1/workflow-1/exec-1/other.png' }, + context + ) + ).rejects.toThrow('File is not available') + await expect( + assertUserFileContentAccess(file, { ...context, workspaceId: 'workspace-2' }) + ).rejects.toThrow('File is not available') + await expect( + assertUserFileContentAccess(file, { ...context, workflowId: 'workflow-2' }) + ).rejects.toThrow('File is not available') + }) + + it('bounds historical file loading even when the messages contain no text', async () => { + queueTableRows(schemaMock.memory, [ + { + data: Array.from({ length: MEMORY.MAX_REPLAY_FILE_REFERENCES + 1 }, () => ({ + role: 'user', + content: '', + files: [storedFile], + })), + }, + ]) + await expect( + memoryService.fetchMemoryMessages({ workspaceId: 'workspace-1' } as ExecutionContext, { + memoryType: 'conversation', + conversationId: 'conversation-1', + }) + ).rejects.toThrow('Use a smaller memory window') + }) + + it('does not carry inline-only or malformed file objects into a later turn', async () => { + queueTableRows(schemaMock.memory, [ + { + data: [ + { + role: 'user', + content: '', + files: [null, { name: 'invalid.png' }, { ...storedFile, key: '', base64: bytes }], + }, + ], + }, + ]) + const messages = await memoryService.fetchMemoryMessages( + { workspaceId: 'workspace-1' } as ExecutionContext, + { memoryType: 'conversation', conversationId: 'conversation-1' } + ) + expect(messages).toEqual([{ role: 'user', content: '' }]) + }) + }) + describe('secret projection', () => { function createContext(registry: ResolvedSecretTraceRegistry) { return { diff --git a/apps/sim/executor/handlers/agent/memory.ts b/apps/sim/executor/handlers/agent/memory.ts index cb12dddfca6..2da2bbec8d9 100644 --- a/apps/sim/executor/handlers/agent/memory.ts +++ b/apps/sim/executor/handlers/agent/memory.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { isPlainRecord } from '@sim/utils/object' import { and, eq, sql } from 'drizzle-orm' +import { isUserFileWithMetadata } from '@/lib/core/utils/user-file' import { bindDurableSecretProvenanceToValue, durableSecretProvenanceFromRegistry, @@ -14,6 +15,7 @@ import { isDurableSecretProvenanceEnforced, reportUnrecordedDurableProvenance, } from '@/lib/execution/durable-secret-provenance-enforcement' +import { mergeFileKeys } from '@/lib/execution/payloads/access-keys' import { redactObjectStrings } from '@/lib/logs/execution/pii-redaction' import { lockMemoryConversationInTx } from '@/lib/memory/locks' import { @@ -23,7 +25,7 @@ import { } from '@/lib/memory/secret-provenance' import { getAccurateTokenCount } from '@/lib/tokenization/accurate' import { MEMORY } from '@/executor/constants' -import type { AgentInputs, Message } from '@/executor/handlers/agent/types' +import type { AgentInputs, FileNameProjection, Message } from '@/executor/handlers/agent/types' import type { ExecutionContext } from '@/executor/types' import { projectResolvedSecretModelContent, @@ -38,7 +40,11 @@ const logger = createLogger('Memory') const MEMORY_CONTENT_REFUSAL = 'Memory content could not be safely projected' export class Memory { - async fetchMemoryMessages(ctx: ExecutionContext, inputs: AgentInputs): Promise { + async fetchMemoryMessages( + ctx: ExecutionContext, + inputs: AgentInputs, + projectedNameByFile?: WeakMap + ): Promise { if (!inputs.memoryType || inputs.memoryType === 'none') { return [] } @@ -76,6 +82,14 @@ export class Memory { messages = stored.messages } + /** Bound historical downloads independently of text-token windows and per-file byte caps. */ + const fileCount = messages.reduce((count, message) => count + (message.files?.length ?? 0), 0) + if (fileCount > MEMORY.MAX_REPLAY_FILE_REFERENCES) { + throw new Error( + `Conversation memory exceeds ${MEMORY.MAX_REPLAY_FILE_REFERENCES} file attachments. Use a smaller memory window.` + ) + } + const selection = await createMemorySecretProvenanceSelector( stored.provenance, stored.messages, @@ -166,7 +180,7 @@ export class Memory { }) } - return Promise.all( + const projectedMessages = await Promise.all( messages.map(async (message) => { const messageProvenance = selectProvenance([message]) const modelRegistry = new ResolvedSecretTraceRegistry( @@ -188,9 +202,15 @@ export class Memory { inputPath: 'messages', }) } - return this.projectMessageForModel(modelRegistry, message) + return this.projectMessageForModel(modelRegistry, message, projectedNameByFile) }) ) + /** Saved references admit only these files; materialization still enforces their scope. */ + mergeFileKeys( + ctx, + projectedMessages.flatMap((message) => message.files?.map((file) => file.key) ?? []) + ) + return projectedMessages } private captureMessagesProvenance( @@ -305,7 +325,25 @@ export class Memory { } } - private projectMessageForModel(registry: ResolvedSecretTraceRegistry, message: Message): Message { + private projectMessageForModel( + registry: ResolvedSecretTraceRegistry, + message: Message, + projectedNameByFile?: WeakMap + ): Message { + for (const file of message.files ?? []) { + const projection = projectResolvedSecretModelContent(file.name, registry) + if (!projection.safe || typeof projection.value !== 'string') { + refuseResolvedSecretProjection({ + site: 'memory.fileNameProjection', + message: MEMORY_CONTENT_REFUSAL, + registry, + inputPath: 'files.name', + }) + } + if (projection.value !== file.name) { + projectedNameByFile?.set(file, { name: projection.value }) + } + } const functionArguments = this.readFunctionCallArguments( message.function_call, registry, @@ -451,9 +489,24 @@ export class Memory { return messages.slice(-limit) } + /** Storage keys survive turns; inline bytes, signed URLs, and provider handles do not. */ private sanitizeMessageForStorage(message: Message): Message { const { files: _files, ...messageWithoutFiles } = message - return messageWithoutFiles + const files = Array.isArray(message.files) + ? message.files + .filter(isUserFileWithMetadata) + .filter((file) => file.key) + .map((file) => ({ + id: file.id, + name: file.name, + key: file.key, + url: '', + size: file.size, + type: file.type, + ...(typeof file.context === 'string' ? { context: file.context } : {}), + })) + : [] + return files.length > 0 ? { ...messageWithoutFiles, files } : messageWithoutFiles } private applyTokenWindow(messages: Message[], maxTokens: number, model?: string): Message[] { diff --git a/apps/sim/executor/handlers/agent/types.ts b/apps/sim/executor/handlers/agent/types.ts index 6ba968f9904..514ecad4c58 100644 --- a/apps/sim/executor/handlers/agent/types.ts +++ b/apps/sim/executor/handlers/agent/types.ts @@ -1,5 +1,11 @@ import type { McpOperationPolicy } from '@/lib/mcp/operation-policy' import type { UserFile } from '@/executor/types' +import type { ResolvedSecretInputPath } from '@/executor/utils/resolved-secret-trace-registry' + +export interface FileNameProjection { + name: string + inputPath?: ResolvedSecretInputPath +} export interface SkillInput { skillId: string diff --git a/apps/sim/lib/memory/message-provenance.test.ts b/apps/sim/lib/memory/message-provenance.test.ts index 0104bb00e1a..af5d95e202a 100644 --- a/apps/sim/lib/memory/message-provenance.test.ts +++ b/apps/sim/lib/memory/message-provenance.test.ts @@ -48,7 +48,7 @@ import { createMemorySecretProvenanceSelector, } from '@/lib/memory/secret-provenance' import { Memory } from '@/executor/handlers/agent/memory' -import type { AgentInputs, Message } from '@/executor/handlers/agent/types' +import type { AgentInputs, FileNameProjection, Message } from '@/executor/handlers/agent/types' import type { ExecutionContext } from '@/executor/types' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' import { memoryAddTool } from '@/tools/memory/add' @@ -196,38 +196,74 @@ describe.each([false, true])('memory message provenance with enforcement %s', (e expect(mocks.logger.error).not.toHaveBeenCalled() }) - it.each(['append', 'seed'] as const)('binds %s messages after removing files', async (mode) => { - const service = new Memory() - const writes = service as unknown as MemoryWrites - const append = vi.spyOn(writes, 'appendMessage').mockResolvedValue(undefined) - const seed = vi.spyOn(writes, 'seedMemoryRecord').mockResolvedValue(undefined) - const registry = new ResolvedSecretTraceRegistry( - [{ name: 'TOKEN', plaintext: SECRET, encryptedValue: 'ciphertext' }], - SCOPE - ) - registry.recordResolved('TOKEN', SECRET) - const message = { - role: 'user', - content: SECRET, - files: [{ id: 'file-1', name: 'document.txt' }], - } as Message - if (mode === 'append') await service.appendToMemory(executionContext(registry), INPUTS, message) - else await service.seedMemory(executionContext(registry), INPUTS, [message]) + it.each(['append', 'seed'] as const)( + 'binds %s messages after removing transient attachment fields', + async (mode) => { + const service = new Memory() + const writes = service as unknown as MemoryWrites + const append = vi.spyOn(writes, 'appendMessage').mockResolvedValue(undefined) + const seed = vi.spyOn(writes, 'seedMemoryRecord').mockResolvedValue(undefined) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: SECRET, encryptedValue: 'ciphertext' }], + SCOPE + ) + registry.recordResolved('TOKEN', SECRET) + const message = { + role: 'user', + content: SECRET, + files: [ + { + id: 'file-1', + name: `${SECRET}.txt`, + key: 'workspace/workspace-1/document.txt', + url: 'https://storage.example.com/signed', + size: 8, + type: 'text/plain', + base64: 'cGF5bG9hZA==', + providerFileId: 'expired-file', + }, + ], + } as Message + if (mode === 'append') + await service.appendToMemory(executionContext(registry), INPUTS, message) + else await service.seedMemory(executionContext(registry), INPUTS, [message]) - const stored = mode === 'append' ? [append.mock.calls[0][2]] : seed.mock.calls[0][2] - const provenance = mode === 'append' ? append.mock.calls[0][3] : seed.mock.calls[0][3] - expect(stored).toEqual([{ role: 'user', content: SECRET }]) - expect(provenance).toMatchObject({ - status: 'exact', - entries: [{ sourceValueHash: hashDurableSecretProvenanceValue(stored[0]) }], - }) - if (provenance?.status !== 'exact') throw new Error('Expected exact provenance') - queueStoredMemory(stored, provenance.entries) - expect((await service.fetchMemoryMessages(executionContext(), INPUTS))[0].content).toBe( - '{{TOKEN}}' - ) - expect(mocks.logger.error).not.toHaveBeenCalled() - }) + const stored = mode === 'append' ? [append.mock.calls[0][2]] : seed.mock.calls[0][2] + const provenance = mode === 'append' ? append.mock.calls[0][3] : seed.mock.calls[0][3] + expect(stored).toEqual([ + { + role: 'user', + content: SECRET, + files: [ + { + id: 'file-1', + name: `${SECRET}.txt`, + key: 'workspace/workspace-1/document.txt', + url: '', + size: 8, + type: 'text/plain', + }, + ], + }, + ]) + expect(provenance).toMatchObject({ + status: 'exact', + entries: [{ sourceValueHash: hashDurableSecretProvenanceValue(stored[0]) }], + }) + if (provenance?.status !== 'exact') throw new Error('Expected exact provenance') + queueStoredMemory(stored, provenance.entries) + const projectedNames = new WeakMap() + const [replayed] = await service.fetchMemoryMessages( + executionContext(), + INPUTS, + projectedNames + ) + expect(replayed.content).toBe('{{TOKEN}}') + expect(replayed.files?.[0].name).toBe(`${SECRET}.txt`) + expect(projectedNames.get(replayed.files![0])).toEqual({ name: '{{TOKEN}}.txt' }) + expect(mocks.logger.error).not.toHaveBeenCalled() + } + ) it.each(['unbound', 'before-file-sanitization'] as const)( 'redacts historical %s entries without refusing the run or exposing telemetry values', From 0734cd0e0264200ed781a2169fe6b8ef5e8401f3 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 14 Sep 2026 15:13:15 -0700 Subject: [PATCH 03/17] feat(slack): switch custom bots to the Sim Search app (#7811) * fix(slack): show shared app installation beside custom bots * feat(slack): switch custom bots to the Sim Search app * fix(slack): show setup completion as a toast * feat(slack): link retired bots to the Sim Search app --- .../api/knowledge/slack/setup/route.test.ts | 10 +- .../organization-search-slack.test.tsx | 207 ++++++++++++- .../components/organization-search-slack.tsx | 66 ++-- .../slack-search-setup-wizard.tsx | 41 ++- .../provider-configuration.test.ts | 36 ++- .../slack-managed-users.test.ts | 1 + .../slack-search/authorization.test.ts | 93 +++++- .../application/slack-search/authorization.ts | 41 ++- .../slack-search/process-message.test.ts | 114 +++++++ .../slack-search/process-message.ts | 39 ++- .../application/slack-search/setup.test.ts | 281 +++++++++++++++++- .../application/slack-search/setup.ts | 120 ++++++-- .../application/slack-search/turns.test.ts | 113 +++++++ .../application/slack-search/turns.ts | 25 +- apps/sim/lib/slack-search/manifest.test.ts | 10 +- apps/sim/lib/slack-search/manifest.ts | 7 - apps/sim/lib/slack-search/messages.ts | 30 ++ apps/sim/lib/slack-search/oauth-state.test.ts | 22 ++ apps/sim/lib/slack-search/oauth-state.ts | 20 +- apps/sim/lib/slack-search/shared-app.test.ts | 18 ++ apps/sim/lib/slack-search/shared-app.ts | 3 +- apps/sim/lib/slack-search/types.ts | 1 + 22 files changed, 1176 insertions(+), 122 deletions(-) create mode 100644 apps/sim/lib/knowledge/application/slack-search/turns.test.ts diff --git a/apps/sim/app/api/knowledge/slack/setup/route.test.ts b/apps/sim/app/api/knowledge/slack/setup/route.test.ts index bf010f0d34a..49c21d524ce 100644 --- a/apps/sim/app/api/knowledge/slack/setup/route.test.ts +++ b/apps/sim/app/api/knowledge/slack/setup/route.test.ts @@ -17,7 +17,7 @@ vi.mock('@/lib/knowledge/application/slack-search/setup', async () => { } }) -import { createSlackSearchManifest } from '@/lib/slack-search/manifest' +import { OrchestrationError } from '@/lib/core/orchestration/types' import { POST as start } from '@/app/api/knowledge/slack/oauth/route' import { POST as prepare } from '@/app/api/knowledge/slack/setup/route' @@ -35,13 +35,13 @@ describe.each([ ['prepare', prepare, mocks.prepare], ['OAuth', start, mocks.start], ] as const)('Slack %s route errors', (_name, route, execute) => { - it('returns an actionable 400 for a non-HTTPS app URL', async () => { - execute.mockImplementation(() => - createSlackSearchManifest(input.name, input.description, 'http://localhost:3000') + it('returns application validation errors', async () => { + execute.mockRejectedValue( + new OrchestrationError('validation', 'Slack app credentials are required') ) const response = await route(createMockRequest('POST', input)) expect(response.status).toBe(400) - expect(await response.json()).toMatchObject({ error: expect.stringContaining('public HTTPS') }) + expect(await response.json()).toMatchObject({ error: 'Slack app credentials are required' }) expect(execute).toHaveBeenCalledOnce() }) diff --git a/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.test.tsx index 5c8679484e7..a350cb46405 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.test.tsx @@ -1,5 +1,6 @@ /** @vitest-environment jsdom */ import { act, type ReactNode } from 'react' +import { ToastProvider } from '@sim/emcn' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SlackSearchInstallationView } from '@/lib/api/contracts/knowledge/slack' @@ -14,6 +15,7 @@ const mocks = vi.hoisted(() => ({ refetch: vi.fn(), copy: vi.fn(), removeError: null as Error | null, + installError: null as Error | null, })) vi.mock('nuqs', () => ({ useQueryState: () => [null, vi.fn()] })) vi.mock('@/components/settings/settings-panel', () => ({ @@ -32,7 +34,12 @@ vi.mock('@/hooks/queries/slack-search', () => ({ error: mocks.removeError, reset: vi.fn(), }), - useStartSlackSearchOAuth: () => ({ mutate: mocks.install, isPending: false, reset: vi.fn() }), + useStartSlackSearchOAuth: () => ({ + mutate: mocks.install, + isPending: false, + error: mocks.installError, + reset: vi.fn(), + }), })) import { OrganizationSearchSlack } from '@/app/o/[organizationId]/settings/components/organization-search-slack' @@ -41,6 +48,7 @@ const installation: SlackSearchInstallationView = { id: 'installation-1', credentialId: 'credential-1', appId: 'A1', + appKind: 'custom', teamId: 'T1', teamName: 'Test workspace', enabled: true, @@ -57,13 +65,16 @@ beforeEach(() => { vi.stubGlobal('navigator', { clipboard: { writeText: mocks.copy } }) mocks.copy.mockReset().mockResolvedValue(undefined) mocks.context.mockReturnValue({ organization: { id: 'org-1' }, viewer: { isAdmin: true } }) - mocks.list.mockReturnValue({ data: { installations: [], bots: [] } }) + mocks.list.mockReturnValue({ + data: { sharedAppAvailable: false, installations: [], bots: [] }, + }) mocks.manifest.mockReturnValue({ data: { manifest: '{}', existingApp: null, createAppUrl: 'https://api.slack.com/apps' }, isPending: false, refetch: mocks.refetch, }) mocks.removeError = null + mocks.installError = null container = document.createElement('div') document.body.appendChild(container) root = createRoot(container) @@ -77,15 +88,23 @@ async function render(installed = false) { if (installed) { mocks.list.mockReturnValue({ data: { + sharedAppAvailable: false, installations: [installation], bots: [{ id: 'credential-1', displayName: 'Sim Search' }], }, }) } - await act(async () => root.render()) + await act(async () => + root.render( + + + + ) + ) } function button(label: string) { - const element = Array.from(document.querySelectorAll('button')).find( + const scope = document.querySelector('[role="dialog"]') ?? document + const element = Array.from(scope.querySelectorAll('button')).find( (element) => element.textContent?.trim() === label ) expect(element, label).toBeDefined() @@ -94,8 +113,8 @@ function button(label: string) { async function click(label: string) { await act(async () => button(label).click()) } -async function action(label: string) { - const trigger = container.querySelector('[aria-label="Sim Search actions"]')! +async function action(label: string, name = 'Sim Search (custom bot)') { + const trigger = container.querySelector(`[aria-label="${name} actions"]`)! await act(async () => { trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) }) @@ -107,6 +126,174 @@ async function action(label: string) { } describe('Slack Search settings and shared wizard', () => { + it.each([ + { state: 'no bots', installations: [] }, + { state: 'custom bots', installations: [installation] }, + ])( + 'installs the official app explicitly with $state and a custom source app', + async ({ installations }) => { + mocks.list.mockReturnValue({ + data: { sharedAppAvailable: true, installations, bots: [] }, + }) + mocks.manifest.mockReturnValue({ + data: { + manifest: '{}', + existingApp: { appId: 'A1', teamId: 'T1' }, + sharedAppId: 'A_SHARED', + createAppUrl: 'https://api.slack.com/apps', + }, + }) + await render() + if (installations.length) { + expect(container).toHaveTextContent('Reconnect required') + expect(container).toHaveTextContent('Sim Search (custom bot)') + await action('Install Sim Search') + } else { + await click('Install Sim Search') + } + expect(document.querySelector('[role="dialog"]')).toHaveTextContent( + 'Install the Sim Search app' + ) + expect(document.querySelectorAll('input')).toHaveLength(0) + expect(mocks.install).not.toHaveBeenCalled() + await click('Continue with Slack') + expect(mocks.install).toHaveBeenCalledExactlyOnceWith( + { + organizationId: 'org-1', + installationId: installations[0]?.id, + name: 'Sim Search', + description: expect.any(String), + mode: 'shared', + }, + expect.any(Object) + ) + mocks.installError = new Error('Slack authorization failed. Try again.') + await render() + expect(document.querySelector('[role="dialog"] [role="alert"]')).toHaveTextContent( + 'Slack authorization failed' + ) + expect(mocks.configure).not.toHaveBeenCalled() + expect(mocks.remove).not.toHaveBeenCalled() + } + ) + + it('reconnects an installed official app without offering a duplicate installation', async () => { + mocks.list.mockReturnValue({ + data: { + sharedAppAvailable: true, + installations: [{ ...installation, appId: 'A_SHARED', appKind: 'shared' }], + bots: [{ id: 'credential-1', displayName: 'Sim Search' }], + }, + }) + mocks.manifest.mockReturnValue({ data: { sharedAppId: 'A_SHARED', existingApp: null } }) + await render() + expect(container).not.toHaveTextContent('Install Sim Search') + await action('Reconnect', 'Sim Search') + await click('Continue with Slack') + expect(mocks.install).toHaveBeenCalledWith( + expect.objectContaining({ mode: 'shared', installationId: installation.id }), + expect.any(Object) + ) + }) + + it('does not switch to custom setup when shared installation becomes unavailable', async () => { + mocks.list.mockReturnValue({ + data: { sharedAppAvailable: true, installations: [installation], bots: [] }, + }) + mocks.manifest.mockReturnValue({ + data: { sharedAppId: null, existingApp: null }, + refetch: mocks.refetch, + }) + await render() + await action('Install Sim Search') + expect(document.querySelector('[role="dialog"] [role="alert"]')).toHaveTextContent( + 'Sim Search installation is unavailable' + ) + expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Create Slack app') + expect(button('Continue with Slack')).toBeDisabled() + expect(mocks.install).not.toHaveBeenCalled() + await click('Retry') + expect(mocks.refetch).toHaveBeenCalledOnce() + }) + + it('allows retrying shared setup after a preparation error with cached data', async () => { + mocks.list.mockReturnValue({ + data: { sharedAppAvailable: true, installations: [installation], bots: [] }, + }) + mocks.manifest.mockReturnValue({ + data: { sharedAppId: 'A_SHARED', existingApp: null }, + error: new Error('Could not load Slack setup'), + refetch: mocks.refetch, + }) + await render() + await action('Install Sim Search') + expect(document.querySelector('[role="dialog"] [role="alert"]')).toHaveTextContent( + 'Could not load Slack setup' + ) + expect(button('Continue with Slack')).toBeDisabled() + await click('Retry') + expect(mocks.refetch).toHaveBeenCalledOnce() + expect(mocks.install).not.toHaveBeenCalled() + }) + + it('does not show official installation when it is unavailable', async () => { + await render(true) + expect(container).not.toHaveTextContent('Install Sim Search') + expect(container).toHaveTextContent('Open in Slack') + }) + + it('prompts the existing custom bot to reconnect when the feature becomes available', async () => { + await render(true) + expect(container).toHaveTextContent('Sim Search (custom bot)') + expect(container).toHaveTextContent('Enabled') + expect(container).not.toHaveTextContent('Reconnect required') + mocks.list.mockReturnValue({ + data: { sharedAppAvailable: true, installations: [installation], bots: [] }, + }) + mocks.manifest.mockReturnValue({ data: { sharedAppId: 'A_SHARED', existingApp: null } }) + await render() + expect(container).toHaveTextContent('Reconnect required') + expect(container).not.toHaveTextContent('Install Sim Search') + expect(mocks.install).not.toHaveBeenCalled() + expect(mocks.configure).not.toHaveBeenCalled() + await action('Install Sim Search') + expect(document.querySelector('[role="dialog"]')).toHaveTextContent( + 'Install the Sim Search app' + ) + expect(button('Continue with Slack')).not.toBeDisabled() + await click('Cancel') + expect(mocks.install).not.toHaveBeenCalled() + expect(mocks.remove).not.toHaveBeenCalled() + }) + + it('shows the native app alongside the retained custom bot after installing', async () => { + mocks.list.mockReturnValue({ + data: { + sharedAppAvailable: true, + installations: [ + { ...installation, enabled: false }, + { + ...installation, + id: 'native-installation', + credentialId: 'native-credential', + appId: 'A_SHARED', + appKind: 'shared', + }, + ], + bots: [], + }, + }) + await render() + expect(container.querySelector('[aria-label="Sim Search (custom bot) actions"]')).not.toBeNull() + expect(container.querySelector('[aria-label="Sim Search actions"]')).not.toBeNull() + expect(container).toHaveTextContent('Disabled') + expect(container).toHaveTextContent('Enabled') + expect(container).not.toHaveTextContent('Reconnect required') + expect(container).not.toHaveTextContent('Install Sim Search') + expect(container.querySelectorAll('a[href*="slack.com/app_redirect"]')).toHaveLength(2) + expect(mocks.install).not.toHaveBeenCalled() + }) + it('starts with one setup action and a Slack app link, with no manifest preview or form', async () => { await render() expect(container.querySelectorAll('button')).toHaveLength(1) @@ -124,13 +311,15 @@ describe('Slack Search settings and shared wizard', () => { it('shows setup errors and blocks progression until the manifest loads', async () => { mocks.manifest.mockReturnValue({ - error: new Error('Slack needs a public HTTPS URL to send messages to Sim.'), + error: new Error('Slack app configuration is unavailable.'), refetch: mocks.refetch, isPending: false, }) await render() await click('Set up') - expect(document.querySelector('[role="alert"]')).toHaveTextContent('public HTTPS') + expect(document.querySelector('[role="alert"]')).toHaveTextContent( + 'Slack app configuration is unavailable.' + ) expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Step 1') expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Continue') await click('Retry') @@ -172,7 +361,7 @@ describe('Slack Search settings and shared wizard', () => { expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Loading Slack setup') if (mode === 'shared') { expect(document.querySelector('[role="dialog"]')).not.toHaveTextContent('Step 1') - await click('Install Sim Search') + await click('Continue with Slack') expect(mocks.install).toHaveBeenCalledWith( expect.objectContaining({ organizationId: 'org-1', mode: 'shared' }), expect.any(Object) diff --git a/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.tsx b/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.tsx index 26426ebd5f2..e7d5920ae19 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/organization-search-slack.tsx @@ -1,7 +1,7 @@ 'use client' -import { useState } from 'react' -import { Chip, ChipConfirmModal, ChipLink, ChipModalError, ChipTag } from '@sim/emcn' +import { useEffect, useRef, useState } from 'react' +import { Chip, ChipConfirmModal, ChipLink, ChipModalError, ChipTag, useToast } from '@sim/emcn' import { useQueryState } from 'nuqs' import { SlackIcon } from '@/components/icons' import { SlackSearchSetupWizard } from '@/components/integrations/slack-search-setup-wizard' @@ -26,6 +26,8 @@ import { /** Organization-owned Search bots are installed through the dedicated OAuth wizard. */ export function OrganizationSearchSlack() { + const setupToastShown = useRef(false) + const { toast } = useToast() const { organization, viewer } = useOrganizationContext() const installations = useSlackSearchInstallations(viewer.isAdmin ? organization.id : undefined) const configure = useConfigureSlackSearch() @@ -35,24 +37,33 @@ export function OrganizationSearchSlack() { slackSetupResultParam.parser ) const [wizard, setWizard] = useState<{ + mode?: 'custom' | 'shared' installationId?: string appId?: string initialName?: string } | null>(null) const [removeTarget, setRemoveTarget] = useState<{ id: string; name: string } | null>(null) + + useEffect(() => { + if (!viewer.isAdmin || setupResult !== 'complete' || setupToastShown.current) return + setupToastShown.current = true + toast.success('Slack connected') + void setSetupResult(null) + }, [viewer.isAdmin, setupResult, setSetupResult, toast]) + if (!viewer.isAdmin) return null const busy = configure.isPending || remove.isPending const bots = installations.data?.bots ?? [] + const canInstallSharedApp = !installations.error && installations.data?.sharedAppAvailable + const sharedTeams = new Set( + installations.data?.installations + .filter((installation) => installation.appKind === 'shared') + .map((installation) => installation.teamId) + ) return (
- {setupResult === 'complete' && ( -
-

Slack is connected and ready to use.

- void setSetupResult(null)}>Dismiss -
- )} {installations.error ? ( setWizard({})}> + setWizard(canInstallSharedApp ? { mode: 'shared' } : {})} + > {installations.data.sharedAppAvailable ? 'Install Sim Search' : 'Set up'} } /> ) : ( installations.data.installations.map((installation) => { - const name = - bots.find((bot) => bot.id === installation.credentialId)?.displayName ?? - installation.teamName - const connectionError = installation.needsValidation - ? 'Reconnect to verify the app’s credentials and permissions.' - : ['delivery_failed', 'assistant_or_delivery_failed'].includes( - installation.lastOutcome ?? '' - ) - ? 'The last reply failed. Check the Slack connection.' - : null + const custom = installation.appKind === 'custom' + const name = custom ? 'Sim Search (custom bot)' : 'Sim Search' + const needsInstall = + canInstallSharedApp && custom && !sharedTeams.has(installation.teamId) + const connectionError = needsInstall + ? null + : installation.needsValidation + ? 'Reconnect to verify the app’s credentials and permissions.' + : ['delivery_failed', 'assistant_or_delivery_failed'].includes( + installation.lastOutcome ?? '' + ) + ? 'The last reply failed. Check the Slack connection.' + : null return ( - {installation.needsValidation + {needsInstall || installation.needsValidation ? 'Reconnect required' : installation.enabled ? 'Enabled' @@ -123,13 +140,18 @@ export function OrganizationSearchSlack() { label={`${name} actions`} actions={[ { - label: 'Reconnect', + label: needsInstall ? 'Install Sim Search' : 'Reconnect', disabled: busy, onSelect: () => setWizard({ + mode: needsInstall ? 'shared' : installation.appKind, installationId: installation.id, appId: installation.appId, - initialName: name, + initialName: + custom && !needsInstall + ? bots.find((bot) => bot.id === installation.credentialId) + ?.displayName + : undefined, }), }, { diff --git a/apps/sim/components/integrations/slack-search-setup-wizard.tsx b/apps/sim/components/integrations/slack-search-setup-wizard.tsx index 3f33eee1fc3..3cc991913e9 100644 --- a/apps/sim/components/integrations/slack-search-setup-wizard.tsx +++ b/apps/sim/components/integrations/slack-search-setup-wizard.tsx @@ -20,6 +20,7 @@ import { useSlackSearchManifest, useStartSlackSearchOAuth } from '@/hooks/querie interface SlackSearchSetupWizardProps { organizationId: string + mode?: 'custom' | 'shared' installationId?: string appId?: string initialName?: string @@ -29,6 +30,7 @@ interface SlackSearchSetupWizardProps { /** App creation, credentials, and consent are one organization-specific setup flow. */ export function SlackSearchSetupWizard({ organizationId, + mode, installationId, appId, initialName, @@ -61,9 +63,12 @@ export function SlackSearchSetupWizard({ } } - const shared = Boolean( - prepare.data?.sharedAppId && (!configuredAppId || configuredAppId === prepare.data.sharedAppId) - ) + const shared = mode + ? mode === 'shared' + : Boolean( + prepare.data?.sharedAppId && + (!configuredAppId || configuredAppId === prepare.data.sharedAppId) + ) function installShared() { oauth.mutate( @@ -144,22 +149,40 @@ export function SlackSearchSetupWizard({ onOpenChange={(open) => { if (!open) onClose() }} - srTitle='Install Sim Search' + srTitle='Install the Sim Search app' + size='sm' > - Install Sim Search + Install the Sim Search app

- Choose your Slack workspace and approve Sim Search. + Add Sim Search to your Slack workspace to ask questions and get answers from your + connected sources.

- {error?.message} + + {error?.message ?? + (!prepare.data.sharedAppId + ? 'Sim Search installation is unavailable. Try again.' + : null)} +
void prepare.refetch(), + disabled: prepare.isFetching, + }, + ] + : undefined + } primaryAction={{ - label: busy ? 'Connecting…' : 'Install Sim Search', - disabled: busy, + label: busy ? 'Connecting…' : 'Continue with Slack', + disabled: busy || !prepare.data.sharedAppId || Boolean(prepare.error), onClick: installShared, }} /> diff --git a/apps/sim/lib/credential-groups/provider-configuration.test.ts b/apps/sim/lib/credential-groups/provider-configuration.test.ts index bd833a7f285..0d5c958b2ad 100644 --- a/apps/sim/lib/credential-groups/provider-configuration.test.ts +++ b/apps/sim/lib/credential-groups/provider-configuration.test.ts @@ -12,6 +12,7 @@ const shared = vi.hoisted(() => ({ flag: vi.fn(), })) vi.mock('@/lib/core/config/env', () => ({ env: shared.env })) +vi.mock('@/lib/core/config/env-flags', () => ({ isHosted: true })) vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: shared.flag })) vi.mock('@/lib/core/security/encryption', () => ({ @@ -39,7 +40,7 @@ const configuration = { beforeEach(() => { resetDbChainMock() shared.env.SLACK_SEARCH_APP_ID = '' - shared.flag.mockResolvedValue(true) + shared.flag.mockReset().mockResolvedValue(true) }) describe('organization Slack app references', () => { @@ -105,6 +106,39 @@ describe('organization Slack app references', () => { else await expect(result).rejects.toThrow('disabled or removed') } ) + it('keeps using the custom app for personal sources after a different native app is installed', async () => { + shared.env.SLACK_SEARCH_APP_ID = 'ANATIVE' + dbChainMockFns.limit + .mockResolvedValueOnce([ + { + encryptedProviderConfiguration: + await encryptCredentialGroupProviderConfiguration(configuration), + }, + ]) + .mockResolvedValueOnce([ + { + id: 'A1', + kind: 'custom', + organizationId: 'org-1', + clientId: 'custom-client', + encryptedClientSecret: 'encrypted:custom-secret', + encryptedSigningSecret: 'encrypted:custom-signing', + }, + ]) + await expect( + getSlackCredentialGroupConfiguration({ + organizationId: 'org-1', + credentialGroupId: 'group-1', + }) + ).resolves.toMatchObject({ + appId: 'A1', + teamId: 'T1', + clientId: 'custom-client', + clientSecret: 'custom-secret', + }) + expect(dbChainMockFns.limit).toHaveBeenCalledTimes(2) + expect(shared.flag).not.toHaveBeenCalled() + }) it('fails when the referenced app is absent from the owning organization', async () => { dbChainMockFns.limit .mockResolvedValueOnce([ diff --git a/apps/sim/lib/credential-groups/slack-managed-users.test.ts b/apps/sim/lib/credential-groups/slack-managed-users.test.ts index 3e32054c599..e6aade21c41 100644 --- a/apps/sim/lib/credential-groups/slack-managed-users.test.ts +++ b/apps/sim/lib/credential-groups/slack-managed-users.test.ts @@ -34,6 +34,7 @@ const shared = vi.hoisted(() => ({ flag: vi.fn(), })) vi.mock('@/lib/core/config/env', () => ({ env: shared.env })) +vi.mock('@/lib/core/config/env-flags', () => ({ isHosted: true })) vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: shared.flag })) vi.mock('@/lib/core/config/redis', () => ({ getRedisClient: () => redis })) diff --git a/apps/sim/lib/knowledge/application/slack-search/authorization.test.ts b/apps/sim/lib/knowledge/application/slack-search/authorization.test.ts index 0835971096e..5e3cf6b52c3 100644 --- a/apps/sim/lib/knowledge/application/slack-search/authorization.test.ts +++ b/apps/sim/lib/knowledge/application/slack-search/authorization.test.ts @@ -6,6 +6,7 @@ const mocks = vi.hoisted(() => ({ installation: vi.fn(), credential: vi.fn(), availability: vi.fn(), + replacement: vi.fn(), })) vi.mock('@/lib/knowledge/application/slack-search/repository', () => ({ findSlackSearchInstallation: mocks.installation, @@ -14,8 +15,15 @@ vi.mock('@/lib/knowledge/application/slack-search/repository', () => ({ vi.mock('@/lib/knowledge/access/availability', () => ({ requireOrganizationSearchAvailable: mocks.availability, })) +vi.mock('@/lib/slack-search/shared-app', () => ({ + requireSlackSearchAppAvailable: vi.fn(), + findSharedSlackSearchInstallation: mocks.replacement, +})) -import { authorizeSlackSearchInstallation } from '@/lib/knowledge/application/slack-search/authorization' +import { + authorizeSlackSearchInstallation, + authorizeSlackSearchRedirect, +} from '@/lib/knowledge/application/slack-search/authorization' const principal: SlackInstallationPrincipal = { kind: 'slack_installation', @@ -41,6 +49,89 @@ beforeEach(() => { mocks.installation.mockResolvedValue(installation) mocks.credential.mockResolvedValue({ version: 'version1', botToken: 'secret' }) mocks.availability.mockResolvedValue(undefined) + mocks.replacement.mockResolvedValue(null) +}) + +describe('retired Slack bot handoff authorization', () => { + const replacement = { + ...installation, + id: 'shared-install', + credentialId: 'shared-credential', + appId: 'ASHARED', + credentialVersion: 'shared-version', + } + beforeEach(() => { + mocks.installation.mockResolvedValue({ ...installation, enabled: false }) + mocks.credential.mockImplementation(async (id) => + id === replacement.credentialId + ? { appKind: 'shared', version: replacement.credentialVersion } + : { appKind: 'custom', version: 'version1', botToken: 'custom-token' } + ) + mocks.replacement.mockResolvedValue(replacement) + }) + + it('authorizes only a handoff to the active shared installation for the same owner and team', async () => { + await expect(authorizeSlackSearchInstallation(principal)).resolves.toBeNull() + await expect(authorizeSlackSearchRedirect(principal)).resolves.toMatchObject({ + installation: { id: 'install1', enabled: false }, + replacement, + secret: { botToken: 'custom-token' }, + }) + expect(mocks.replacement).toHaveBeenCalledWith('org1') + expect(mocks.credential).toHaveBeenLastCalledWith('shared-credential', 'org1') + }) + + it.each([null, { ...installation, enabled: true }])( + 'ignores missing or active old bots: %j', + async (current) => { + mocks.installation.mockResolvedValue(current) + await expect(authorizeSlackSearchRedirect(principal)).resolves.toBeNull() + expect(mocks.replacement).not.toHaveBeenCalled() + } + ) + + it.each([ + null, + { ...replacement, organizationId: 'another-org' }, + { ...replacement, teamId: 'TOTHER' }, + { ...replacement, appId: 'A1' }, + ])('ignores unavailable or mismatched replacements: %j', async (target) => { + mocks.replacement.mockResolvedValue(target) + await expect(authorizeSlackSearchRedirect(principal)).resolves.toBeNull() + expect(mocks.credential).toHaveBeenCalledTimes(1) + }) + + it('does not redirect a disabled shared app', async () => { + mocks.credential.mockResolvedValue({ appKind: 'shared', version: 'version1' }) + await expect(authorizeSlackSearchRedirect(principal)).resolves.toBeNull() + expect(mocks.replacement).not.toHaveBeenCalled() + }) + + it.each([{ teamId: 'TOTHER' }, { appId: 'AOTHER' }, { credentialVersion: 'stale' }])( + 'rejects forged installation identity: %j', + async (change) => { + await expect(authorizeSlackSearchRedirect({ ...principal, ...change })).rejects.toThrow( + 'binding' + ) + expect(mocks.replacement).not.toHaveBeenCalled() + } + ) + + it('rejects old queued work after the installation changes', async () => { + await expect( + authorizeSlackSearchRedirect(principal, { installationId: 'install1', revision: 'old' }) + ).rejects.toThrow('binding') + expect(mocks.replacement).not.toHaveBeenCalled() + }) + + it('rejects revoked Search access and replacement credential rotation', async () => { + mocks.availability.mockRejectedValueOnce(new Error('Search disabled')) + await expect(authorizeSlackSearchRedirect(principal)).rejects.toThrow('Search disabled') + expect(mocks.credential).not.toHaveBeenCalled() + mocks.credential.mockResolvedValueOnce({ appKind: 'custom', version: 'version1' }) + mocks.credential.mockResolvedValueOnce({ appKind: 'shared', version: 'rotated' }) + await expect(authorizeSlackSearchRedirect(principal)).rejects.toThrow('revalidation') + }) }) describe('Slack Search installation authorization', () => { it('rejects human principals before protected lookup', async () => { diff --git a/apps/sim/lib/knowledge/application/slack-search/authorization.ts b/apps/sim/lib/knowledge/application/slack-search/authorization.ts index 37bc713ee26..7c495662add 100644 --- a/apps/sim/lib/knowledge/application/slack-search/authorization.ts +++ b/apps/sim/lib/knowledge/application/slack-search/authorization.ts @@ -4,8 +4,12 @@ import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/avail import { findSlackSearchInstallation, loadSlackSearchCredential, + type SlackSearchInstallation, } from '@/lib/knowledge/application/slack-search/repository' -import { requireSlackSearchAppAvailable } from '@/lib/slack-search/shared-app' +import { + findSharedSlackSearchInstallation, + requireSlackSearchAppAvailable, +} from '@/lib/slack-search/shared-app' export function requireSlackInstallationPrincipal( principal: Principal @@ -29,6 +33,14 @@ export async function authorizeSlackSearchInstallation( requireSlackInstallationPrincipal(principal) const installation = await findSlackSearchInstallation(principal.credentialId) if (!installation || !installation.enabled) return null + return authorizeSlackSearchBinding(principal, installation, expected) +} + +async function authorizeSlackSearchBinding( + principal: SlackInstallationPrincipal, + installation: SlackSearchInstallation, + expected?: { installationId: string; revision: string } +) { if ( installation.appId !== principal.appId || installation.teamId !== principal.teamId || @@ -48,3 +60,30 @@ export async function authorizeSlackSearchInstallation( throw new OrchestrationError('forbidden', 'Slack bot requires revalidation') return { installation, secret } } + +/** A retired custom bot may only point to the active shared app in the same organization and team. */ +export async function authorizeSlackSearchRedirect( + principal: Principal, + expected?: { installationId: string; revision: string } +) { + requireSlackInstallationPrincipal(principal) + const installation = await findSlackSearchInstallation(principal.credentialId) + if (!installation || installation.enabled) return null + const context = await authorizeSlackSearchBinding(principal, installation, expected) + if (context.secret.appKind !== 'custom') return null + const replacement = await findSharedSlackSearchInstallation(installation.organizationId) + if ( + !replacement || + replacement.organizationId !== installation.organizationId || + replacement.teamId !== installation.teamId || + replacement.appId === installation.appId + ) + return null + const secret = await loadSlackSearchCredential( + replacement.credentialId, + installation.organizationId + ) + if (secret.version !== replacement.credentialVersion) + throw new OrchestrationError('forbidden', 'Slack bot requires revalidation') + return { ...context, replacement } +} diff --git a/apps/sim/lib/knowledge/application/slack-search/process-message.test.ts b/apps/sim/lib/knowledge/application/slack-search/process-message.test.ts index 98d1f2036bb..3a24709634e 100644 --- a/apps/sim/lib/knowledge/application/slack-search/process-message.test.ts +++ b/apps/sim/lib/knowledge/application/slack-search/process-message.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ authorize: vi.fn(), + redirect: vi.fn(), persist: vi.fn(), dispatch: vi.fn(), assistant: vi.fn(), @@ -13,6 +14,7 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/lib/knowledge/application/slack-search/authorization', () => ({ requireSlackInstallationPrincipal: vi.fn(), authorizeSlackSearchInstallation: mocks.authorize, + authorizeSlackSearchRedirect: mocks.redirect, })) vi.mock('@/lib/knowledge/application/slack-search/assistant', () => ({ runSlackSearchAssistant: mocks.assistant, @@ -62,10 +64,122 @@ beforeEach(() => { secret: { botToken: 'test-token' }, }) mocks.persist.mockResolvedValue('turn1') + mocks.redirect.mockResolvedValue(null) mocks.route.mockImplementation(async (_principal, { job }) => job) mocks.post.mockResolvedValue({ status: 200, data: { ok: true } }) }) +describe('retired bot handoff', () => { + const redirect = { + installation: { id: 'i1', revision: 'r1', botUserId: 'UBOT' }, + replacement: { appId: 'ASHARED' }, + secret: { botToken: 'old-bot-token' }, + } + const job: SlackSearchJob = { + installationId: 'i1', + revision: 'r1', + credentialId: 'c1', + credentialVersion: 'v1', + receivedAt: principal.receivedAt.getTime(), + redirectAppId: 'ASHARED', + message: { ...message, query: '' }, + } + beforeEach(() => { + mocks.authorize.mockResolvedValue(null) + mocks.redirect.mockResolvedValue(redirect) + }) + const respond = (overrides: Partial = {}) => + respondToSlackSearchMessage.execute({ + principal, + input: { + job: { ...job, ...overrides }, + turnId: 'turn1', + leaseId: 'lease1', + controller: new AbortController(), + }, + }) + + it('queues the handoff through the existing deduplicated turn path without saving the question', async () => { + await receiveSlackSearchMessage.execute({ principal, input: message }) + expect(mocks.persist).toHaveBeenCalledWith(job) + expect(mocks.persist.mock.invocationCallOrder[0]).toBeLessThan( + mocks.dispatch.mock.invocationCallOrder[0] + ) + expect(mocks.post).not.toHaveBeenCalled() + expect(mocks.assistant).not.toHaveBeenCalled() + }) + + it.each([undefined, '1700000000.000001'])( + 'links from the original DM thread: %s', + async (threadTs) => { + await respond({ message: { ...job.message, threadTs } }) + expect(mocks.redirect).toHaveBeenCalledWith( + principal, + expect.objectContaining({ revision: 'r1' }) + ) + expect(mocks.post).toHaveBeenCalledWith( + 'old-bot-token', + expect.objectContaining({ + channel: 'D1', + thread_ts: threadTs ?? message.messageTs, + text: expect.stringContaining('https://slack.com/app_redirect?app=ASHARED&team=T1'), + blocks: expect.arrayContaining([ + expect.objectContaining({ + elements: [ + expect.objectContaining({ + text: { type: 'plain_text', text: 'Open Sim Search' }, + url: 'https://slack.com/app_redirect?app=ASHARED&team=T1', + }), + ], + }), + ]), + }), + expect.any(AbortSignal) + ) + expect(mocks.route).not.toHaveBeenCalled() + expect(mocks.assistant).not.toHaveBeenCalled() + } + ) + + it('keeps a manually disabled bot quiet without an active replacement', async () => { + mocks.redirect.mockResolvedValue(null) + await receiveSlackSearchMessage.execute({ principal, input: message }) + expect(mocks.persist).not.toHaveBeenCalled() + }) + + it.each([{ channelId: 'C1' }, { userId: 'UBOT' }])( + 'ignores mentions and bot messages: %j', + async (change) => { + await receiveSlackSearchMessage.execute({ principal, input: { ...message, ...change } }) + expect(mocks.persist).not.toHaveBeenCalled() + } + ) + + it.each([null, { ...redirect, replacement: { appId: 'ADIFFERENT' } }])( + 'rechecks replacement availability before delivery: %j', + async (current) => { + mocks.redirect.mockResolvedValue(current) + await expect(respond()).rejects.toThrow('replacement changed') + expect(mocks.post).not.toHaveBeenCalled() + } + ) + + it('does not deliver after losing its durable claim', async () => { + mocks.lease.mockRejectedValueOnce(new Error('lease lost')) + await expect(respond()).rejects.toThrow('lease lost') + expect(mocks.post).not.toHaveBeenCalled() + }) + + it('does not retry failed or ambiguous sends', async () => { + mocks.post.mockResolvedValueOnce({ status: 200, data: { ok: false } }) + await expect(respond()).rejects.toThrow('handoff') + mocks.post.mockRejectedValueOnce(new Error('response lost')) + await expect(respond()).rejects.toThrow('response lost') + expect(mocks.post).toHaveBeenCalledTimes(2) + expect(mocks.assistant).not.toHaveBeenCalled() + }) +}) + describe('Slack Search question validation', () => { function respond(overrides: Partial = {}) { return respondToSlackSearchMessage.execute({ diff --git a/apps/sim/lib/knowledge/application/slack-search/process-message.ts b/apps/sim/lib/knowledge/application/slack-search/process-message.ts index e5b38ee3457..76848dd8729 100644 --- a/apps/sim/lib/knowledge/application/slack-search/process-message.ts +++ b/apps/sim/lib/knowledge/application/slack-search/process-message.ts @@ -5,6 +5,7 @@ import { postSlackMessage } from '@/lib/internal/slack/client' import { runSlackSearchAssistant } from '@/lib/knowledge/application/slack-search/assistant' import { authorizeSlackSearchInstallation, + authorizeSlackSearchRedirect, requireSlackInstallationPrincipal, } from '@/lib/knowledge/application/slack-search/authorization' import { routeSlackSearchMentionToDm } from '@/lib/knowledge/application/slack-search/mention' @@ -14,7 +15,7 @@ import { requireSlackSearchTurnLease, } from '@/lib/knowledge/application/slack-search/turns' import { SLACK_SEARCH_QUERY_TOO_LONG } from '@/lib/slack-search/constants' -import { slackSearchReply } from '@/lib/slack-search/messages' +import { renderSlackSearchRedirect, slackSearchReply } from '@/lib/slack-search/messages' import type { SlackSearchJob, SlackSearchMessage } from '@/lib/slack-search/types' import { slackSearchThreadTimestamp } from '@/lib/slack-search/types' @@ -51,7 +52,23 @@ export const receiveSlackSearchMessage: OperationUseCase< requireSlackInstallationPrincipal(principal) requireMessageBinding(principal, input) const context = await authorizeSlackSearchInstallation(principal) - if (!context || input.userId === context.installation.botUserId) return + if (!context) { + if (!input.channelId.startsWith('D') || input.command || input.origin) return + const redirect = await authorizeSlackSearchRedirect(principal) + if (!redirect || input.userId === redirect.installation.botUserId) return + const turnId = await persistSlackSearchTurn({ + installationId: redirect.installation.id, + revision: redirect.installation.revision, + credentialId: principal.credentialId, + credentialVersion: principal.credentialVersion, + receivedAt: principal.receivedAt.getTime(), + redirectAppId: redirect.replacement.appId, + message: { ...input, query: '', queryTooLong: false }, + }) + await dispatchSlackSearchTurn(turnId) + return turnId + } + if (input.userId === context.installation.botUserId) return const mention = !input.channelId.startsWith('D') const query = mention ? input.query.replaceAll(`<@${context.installation.botUserId}>`, '').trim() @@ -86,6 +103,24 @@ export const respondToSlackSearchMessage: OperationUseCase< async execute({ principal, input }) { requireSlackInstallationPrincipal(principal) requireMessageBinding(principal, input.job.message) + if (input.job.redirectAppId) { + const { job, controller } = input + if (!job.message.channelId.startsWith('D') || job.message.command || job.message.origin) + throw new OrchestrationError('forbidden', 'Slack app redirects require a direct message') + await requireSlackSearchTurnLease(input.turnId, input.leaseId) + const context = await authorizeSlackSearchRedirect(principal, job) + if (!context || context.replacement.appId !== job.redirectAppId) + throw new OrchestrationError('forbidden', 'Slack Search replacement changed') + controller.signal.throwIfAborted() + const response = await postSlackMessage( + context.secret.botToken, + renderSlackSearchRedirect(job.message, context.replacement.appId), + AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]) + ) + if (response.status !== 200 || response.data.ok !== true) + throw new Error('Could not deliver the Slack app handoff') + return + } if (!input.job.message.queryTooLong && !input.job.message.query) throw new OrchestrationError( 'validation', diff --git a/apps/sim/lib/knowledge/application/slack-search/setup.test.ts b/apps/sim/lib/knowledge/application/slack-search/setup.test.ts index 214f5f3a290..da736fa278a 100644 --- a/apps/sim/lib/knowledge/application/slack-search/setup.test.ts +++ b/apps/sim/lib/knowledge/application/slack-search/setup.test.ts @@ -1,5 +1,6 @@ /** @vitest-environment node */ import { db } from '@sim/db' +import { credential, slackSearchInstallation } from '@sim/db/schema' import { beforeEach, describe, expect, it, vi } from 'vitest' const m = vi.hoisted(() => ({ @@ -17,6 +18,10 @@ const m = vi.hoisted(() => ({ revoke: vi.fn(), validateGrant: vi.fn(), ensureGroup: vi.fn(), + memberApps: vi.fn(), + adoptMemberApp: vi.fn(), + insert: vi.fn(), + update: vi.fn(), })) vi.mock('@/lib/slack-search/shared-app', () => ({ readSharedSlackSearchApp: m.shared })) vi.mock('@sim/audit', () => ({ @@ -55,8 +60,8 @@ vi.mock('@/lib/internal/slack/oauth', () => ({ })) vi.mock('@/lib/credential-groups/service', () => ({ ensureWorkspaceAccountsGroup: m.ensureGroup })) vi.mock('@/lib/credential-groups/organization-slack-app', () => ({ - loadOrganizationSlackMemberApps: async () => [], - adoptOrganizationSlackMemberApp: vi.fn(), + loadOrganizationSlackMemberApps: m.memberApps, + adoptOrganizationSlackMemberApp: m.adoptMemberApp, })) vi.mock('@/lib/internal/slack/search-client', () => ({ verifySlackSearchBot: m.verify, @@ -98,6 +103,7 @@ beforeEach(() => { m.revoke.mockResolvedValue(undefined) m.validateGrant.mockReset() m.ensureGroup.mockResolvedValue({ id: 'accounts' }) + m.memberApps.mockReset().mockResolvedValue([]) m.baseUrl.mockReturnValue('https://sim.test') m.membership.mockResolvedValue([{ role: 'admin' }]) m.rows.mockReset().mockResolvedValue([]) @@ -138,28 +144,31 @@ beforeEach(() => { const tx = { execute: vi.fn(), select: () => txQuery, - insert: () => txQuery, - update: () => txQuery, + insert: m.insert.mockReturnValue(txQuery), + update: m.update.mockReturnValue(txQuery), } vi.mocked(db.transaction).mockImplementation(async (callback) => callback(tx as Parameters[0]>[0]) ) }) describe('Search OAuth installation', () => { - it('fails setup and OAuth with actionable validation before storing secrets on localhost', async () => { + it('prepares setup and starts OAuth using the configured app origin', async () => { m.baseUrl.mockReturnValue('http://localhost:3000') const input = { organizationId: 'org1', name: 'Sim Search', description: 'Search with sources' } - await expect(prepareSlackSearchSetup.execute({ principal, input })).rejects.toMatchObject({ - code: 'validation', - message: expect.stringContaining('public HTTPS'), - }) + await expect(prepareSlackSearchSetup.execute({ principal, input })).resolves.toHaveProperty( + 'manifest' + ) await expect( startSlackSearchSetup.execute({ principal, input: { ...input, clientId: 'client', clientSecret: 'secret', signingSecret: 'signing' }, }) - ).rejects.toMatchObject({ code: 'validation' }) - expect(m.store).not.toHaveBeenCalled() + ).resolves.toHaveProperty('authorizationUrl') + expect(m.store).toHaveBeenCalledWith( + expect.objectContaining({ + redirectUri: 'http://localhost:3000/api/knowledge/slack/oauth/callback', + }) + ) expect(m.exchange).not.toHaveBeenCalled() expect(db.transaction).not.toHaveBeenCalled() }) @@ -336,6 +345,256 @@ describe('shared app completion', () => { expect(JSON.stringify(stored)).not.toContain('environment-secret') }) + describe('custom bot transition', () => { + const customInstallation = { + id: 'custom-installation', + revision: 'custom-revision', + organizationId: 'org1', + credentialId: 'custom-credential', + appId: 'ACUSTOM', + slackAppId: 'ACUSTOM', + teamId: 'T1', + enabled: true, + } + const customApp = { + id: 'ACUSTOM', + kind: 'custom', + organizationId: 'org1', + revision: 'custom-app-revision', + } + const memberApp = { appId: 'ACUSTOM', teamId: 'T1' } + const input = { + organizationId: 'org1', + installationId: customInstallation.id, + mode: 'shared' as const, + name: 'Sim Search', + description: 'Search', + } + + beforeEach(() => { + m.memberApps.mockResolvedValue([ + { configuration: { slack: { ...memberApp, scopes: ['im:history'] } } }, + ]) + m.consume.mockResolvedValue({ + ...attempt, + sharedApp: { id: sharedApp.id, revision: sharedApp.revision }, + customInstallation, + memberApp, + }) + }) + + function queueTransitionRows(custom = customInstallation) { + m.rows + .mockResolvedValueOnce([sharedApp]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([custom]) + .mockResolvedValueOnce([]) + } + + it('binds the old installation and workspace without changing anything before approval', async () => { + m.membership + .mockResolvedValueOnce([{ role: 'admin' }]) + .mockResolvedValueOnce([customInstallation]) + .mockResolvedValueOnce([customApp]) + const result = await startSlackSearchSetup.execute({ principal, input }) + const url = new URL(result.authorizationUrl) + expect(url.searchParams.get('team')).toBe('T1') + expect(url.searchParams.get('client_id')).toBe(sharedApp.clientId) + expect(url.searchParams.has('user_scope')).toBe(false) + expect(m.store).toHaveBeenCalledWith( + expect.objectContaining({ + customInstallation: { + id: customInstallation.id, + revision: customInstallation.revision, + credentialId: customInstallation.credentialId, + appId: customInstallation.appId, + teamId: 'T1', + appRevision: customApp.revision, + }, + memberApp, + }) + ) + expect(m.store.mock.calls[0][0]).not.toHaveProperty('installation') + expect(db.transaction).not.toHaveBeenCalled() + }) + + it('keeps the old row and credentials while activating a new bot in one transaction', async () => { + queueTransitionRows() + await complete() + expect(db.transaction).toHaveBeenCalledOnce() + expect(m.update).toHaveBeenCalledExactlyOnceWith(slackSearchInstallation) + expect(m.set).toHaveBeenCalledExactlyOnceWith({ + enabled: false, + revision: expect.not.stringMatching(customInstallation.revision), + updatedAt: expect.any(Date), + }) + expect(m.insert).toHaveBeenCalledWith(credential) + expect(m.values).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.not.stringMatching(customInstallation.credentialId), + organizationId: 'org1', + slackAppId: 'A1', + workspaceId: null, + }) + ) + expect(m.values).toHaveBeenLastCalledWith( + expect.objectContaining({ + id: expect.not.stringMatching(customInstallation.id), + appId: 'A1', + teamId: 'T1', + enabled: true, + }) + ) + expect(m.adoptMemberApp).not.toHaveBeenCalled() + expect(m.ensureGroup).not.toHaveBeenCalled() + expect(m.revoke).not.toHaveBeenCalled() + expect(m.audit).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ previousInstallationId: customInstallation.id }), + }) + ) + expect(m.set.mock.invocationCallOrder[0]).toBeLessThan( + m.values.mock.invocationCallOrder.at(-1)! + ) + }) + + it('refuses a foreign installation before issuing OAuth state', async () => { + m.membership.mockResolvedValueOnce([{ role: 'admin' }]).mockResolvedValueOnce([]) + await expect(startSlackSearchSetup.execute({ principal, input })).rejects.toThrow('not found') + expect(m.store).not.toHaveBeenCalled() + }) + + it('rechecks the rollout gate before disabling the old bot', async () => { + queueTransitionRows() + m.shared.mockResolvedValueOnce(sharedApp).mockResolvedValueOnce(null) + await expect(complete()).rejects.toThrow('configuration changed') + expect(m.update).not.toHaveBeenCalled() + expect(m.values).not.toHaveBeenCalled() + }) + + it('rejects another active workspace binding even during a custom transition', async () => { + m.rows + .mockResolvedValueOnce([sharedApp]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([customInstallation]) + .mockResolvedValueOnce([{ id: 'another-installation' }]) + await expect(complete()).rejects.toThrow('already has an active Search installation') + expect(m.update).not.toHaveBeenCalled() + expect(m.values).not.toHaveBeenCalled() + }) + + it('rejects a duplicate completion without revoking the already installed native bot', async () => { + m.rows + .mockResolvedValueOnce([sharedApp]) + .mockResolvedValueOnce([{ id: 'native-installation', organizationId: 'org1' }]) + .mockResolvedValueOnce([{ id: 'native-installation' }]) + await expect(complete()).rejects.toThrow('already connected') + expect(m.update).not.toHaveBeenCalled() + expect(m.revoke).not.toHaveBeenCalled() + }) + + it('fails the transaction if saving the new installation fails after disabling the custom bot', async () => { + queueTransitionRows() + const baseValues = m.values.getMockImplementation()! + m.values.mockImplementation((value) => { + if (value.enabled === true) throw new Error('installation write failed') + return baseValues(value) + }) + await expect(complete()).rejects.toThrow('installation write failed') + expect(m.set).toHaveBeenCalledWith(expect.objectContaining({ enabled: false })) + await expect(vi.mocked(db.transaction).mock.results[0].value).rejects.toThrow( + 'installation write failed' + ) + expect(m.audit).not.toHaveBeenCalled() + expect(m.revoke).toHaveBeenCalledWith('bot-token') + }) + + it('leaves the custom bot untouched when the admin cancels Slack consent', async () => { + await expect( + completeSlackSearchSetup.execute({ + principal, + input: { state: 'state', error: 'access_denied' }, + }) + ).rejects.toThrow('not authorized') + expect(m.exchange).not.toHaveBeenCalled() + expect(db.transaction).not.toHaveBeenCalled() + }) + + it('rejects a transition to a different Slack workspace', async () => { + m.exchange.mockResolvedValue({ + app_id: 'A1', + team: { id: 'T2' }, + bot_user_id: 'UBOT', + access_token: 'bot-token', + }) + m.verify.mockResolvedValue({ ...identity, teamId: 'T2' }) + await expect(complete()).rejects.toThrow('same Slack workspace') + expect(m.update).not.toHaveBeenCalled() + expect(m.values).not.toHaveBeenCalled() + }) + + it.each([ + { revision: 'changed' }, + { organizationId: 'other-org' }, + { credentialId: 'another-credential' }, + { appId: 'another-app' }, + ])('rejects a stale or foreign custom installation: %j', async (changed) => { + queueTransitionRows({ ...customInstallation, ...changed }) + await expect(complete()).rejects.toThrow('custom bot changed') + expect(m.update).not.toHaveBeenCalled() + expect(m.values).not.toHaveBeenCalled() + }) + + it('refuses an installation removed while OAuth was open', async () => { + m.rows.mockResolvedValueOnce([sharedApp]).mockResolvedValueOnce([]).mockResolvedValueOnce([]) + await expect(complete()).rejects.toThrow('custom bot changed') + expect(m.update).not.toHaveBeenCalled() + }) + + it('refuses a changed personal source configuration without touching its grants', async () => { + queueTransitionRows() + m.memberApps.mockResolvedValue([]) + await expect(complete()).rejects.toThrow('source configuration changed') + expect(m.update).not.toHaveBeenCalled() + expect(m.adoptMemberApp).not.toHaveBeenCalled() + expect(m.ensureGroup).not.toHaveBeenCalled() + }) + + it('rejects a transition when the shared app is disabled', async () => { + m.shared.mockResolvedValue(null) + await expect(complete()).rejects.toThrow('configuration changed') + expect(m.exchange).not.toHaveBeenCalled() + expect(m.update).not.toHaveBeenCalled() + }) + + it('reconnects the native bot later without changing the custom source configuration', async () => { + const installed = { + id: 'native-installation', + revision: 'native-revision', + credentialId: 'native-credential', + appId: 'A1', + teamId: 'T1', + organizationId: 'org1', + } + m.consume.mockResolvedValue({ + ...attempt, + sharedApp: { id: sharedApp.id, revision: sharedApp.revision }, + installation: installed, + memberApp, + }) + m.rows + .mockResolvedValueOnce([sharedApp]) + .mockResolvedValueOnce([installed]) + .mockResolvedValueOnce([]) + await complete() + expect(m.ensureGroup).not.toHaveBeenCalled() + expect(m.adoptMemberApp).not.toHaveBeenCalled() + expect(m.set).toHaveBeenCalledWith( + expect.objectContaining({ slackAppId: 'A1', enabled: true }) + ) + }) + }) + it('creates shared identity without app secrets and installs atomically without registration', async () => { m.rows .mockResolvedValueOnce([]) diff --git a/apps/sim/lib/knowledge/application/slack-search/setup.ts b/apps/sim/lib/knowledge/application/slack-search/setup.ts index b2e259a2a65..eda969c1afc 100644 --- a/apps/sim/lib/knowledge/application/slack-search/setup.ts +++ b/apps/sim/lib/knowledge/application/slack-search/setup.ts @@ -12,6 +12,7 @@ import { loadOrganizationSlackMemberApps, } from '@/lib/credential-groups/organization-slack-app' import { configureSharedSlackMemberApp } from '@/lib/credential-groups/shared-slack-app' +import type { DbOrTx } from '@/lib/db/types' import { exchangeSlackBotAuthorization, revokeSlackBotAuthorization, @@ -54,8 +55,8 @@ interface CompleteInput { error?: string } -async function existingMemberApp(organizationId: string) { - const rows = await loadOrganizationSlackMemberApps(organizationId) +async function existingMemberApp(organizationId: string, executor: DbOrTx = db) { + const rows = await loadOrganizationSlackMemberApps(organizationId, executor) const configurations = rows.flatMap((row) => row.configuration.slack ? [row.configuration.slack] : [] ) @@ -107,7 +108,6 @@ export const startSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ if (principal.kind !== 'session') throw new Error('Slack setup requires a browser session') await requireOrganizationSearchAvailable(context.organizationId) const origin = getBaseUrl() - createSlackSearchManifest(input.name, input.description, origin) const member = await existingMemberApp(context.organizationId) const [installation] = input.installationId ? await db @@ -131,7 +131,10 @@ export const startSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ .limit(1) : [] const shared = input.mode === 'shared' - if (savedApp && (savedApp.kind === 'shared') !== shared) + if (installation?.slackAppId && !savedApp) + throw new OrchestrationError('conflict', 'Slack app configuration is missing') + const transitioning = shared && installation && savedApp?.kind !== 'shared' + if (savedApp?.kind === 'shared' && !shared) throw new OrchestrationError( 'conflict', 'Remove the existing installation before switching Slack apps' @@ -145,10 +148,10 @@ export const startSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ 'validation', 'Shared Slack app setup is unavailable or contains custom credentials' ) - if (shared && member.app && member.app.appId !== app?.id) + if (shared && installation && member.app && member.app.teamId !== installation.teamId) throw new OrchestrationError( 'conflict', - 'Remove the previous Slack source configuration before switching apps; members must reconnect' + 'Install Sim Search in the Slack workspace used for member indexing' ) const clientId = input.clientId ?? app?.clientId if (!clientId) throw new OrchestrationError('validation', 'Slack Client ID is required') @@ -172,6 +175,16 @@ export const startSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ appCredentials = { encryptedClientSecret, encryptedSigningSecret } } const redirectUri = new URL(SLACK_SEARCH_CALLBACK_PATH, origin).href + const installationSnapshot = installation + ? { + id: installation.id, + revision: installation.revision, + credentialId: installation.credentialId, + appId: installation.appId, + teamId: installation.teamId, + ...(savedApp ? { appRevision: savedApp.revision } : {}), + } + : undefined const state = await storeSlackSearchOAuthAttempt({ ...appCredentials, userId: principal.userId, @@ -183,17 +196,10 @@ export const startSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ clientId, redirectUri, createdAt: Date.now(), - ...(installation - ? { - installation: { - id: installation.id, - revision: installation.revision, - credentialId: installation.credentialId, - appId: installation.appId, - teamId: installation.teamId, - ...(app ? { appRevision: app.revision } : {}), - }, - } + ...(installationSnapshot + ? transitioning + ? { customInstallation: installationSnapshot } + : { installation: installationSnapshot } : {}), }) const url = new URL('https://slack.com/oauth/v2/authorize') @@ -264,6 +270,8 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ ) await requireOrganizationSearchAvailable(context.organizationId) const { attempt } = context + if (attempt.customInstallation && (!attempt.sharedApp || attempt.installation)) + throw new OrchestrationError('validation', 'Invalid Slack app transition') let clientSecret: string if (attempt.sharedApp) { const app = await readSharedSlackSearchApp() @@ -322,9 +330,16 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ attempt.installation.teamId !== identity.teamId) ) throw new OrchestrationError('conflict', 'Reconnect the same Slack app and workspace') + if ( + attempt.customInstallation && + (attempt.customInstallation.teamId !== identity.teamId || + attempt.customInstallation.appId === identity.appId) + ) + throw new OrchestrationError('conflict', 'Install Sim Search in the same Slack workspace') if ( attempt.memberApp && - (attempt.memberApp.appId !== identity.appId || attempt.memberApp.teamId !== identity.teamId) + ((!attempt.sharedApp && attempt.memberApp.appId !== identity.appId) || + attempt.memberApp.teamId !== identity.teamId) ) throw new OrchestrationError( 'conflict', @@ -409,6 +424,32 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ 'conflict', 'This app is already connected. Use Reconnect on its existing installation.' ) + const [customInstallation] = attempt.customInstallation + ? await tx + .select() + .from(slackSearchInstallation) + .where( + and( + eq(slackSearchInstallation.id, attempt.customInstallation.id), + eq(slackSearchInstallation.organizationId, context.organizationId) + ) + ) + .for('update') + .limit(1) + : [] + if ( + attempt.customInstallation && + (!customInstallation || + customInstallation.organizationId !== context.organizationId || + customInstallation.revision !== attempt.customInstallation.revision || + customInstallation.credentialId !== attempt.customInstallation.credentialId || + customInstallation.appId !== attempt.customInstallation.appId || + customInstallation.teamId !== identity.teamId) + ) + throw new OrchestrationError( + 'conflict', + 'The custom bot changed during setup. Start setup again.' + ) const [active] = await tx .select({ id: slackSearchInstallation.id }) .from(slackSearchInstallation) @@ -416,7 +457,8 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ and( eq(slackSearchInstallation.teamId, identity.teamId), eq(slackSearchInstallation.enabled, true), - existing ? ne(slackSearchInstallation.id, existing.id) : undefined + existing ? ne(slackSearchInstallation.id, existing.id) : undefined, + customInstallation ? ne(slackSearchInstallation.id, customInstallation.id) : undefined ) ) .limit(1) @@ -476,14 +518,27 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ .values(appValues) .onConflictDoUpdate({ target: slackApp.id, set: appValues }) } - await adoptOrganizationSlackMemberApp( - tx, - context.organizationId, - identity.appId, - identity.teamId, - attempt.clientId + const member = await existingMemberApp(context.organizationId, tx) + if ( + member.app?.appId !== attempt.memberApp?.appId || + member.app?.teamId !== attempt.memberApp?.teamId ) - if (attempt.sharedApp) + throw new OrchestrationError( + 'conflict', + 'Slack source configuration changed during setup' + ) + /** A bot transition leaves existing personal grants and indexing configuration untouched. */ + const preserveMemberApp = + attempt.sharedApp && member.app && member.app.appId !== identity.appId + if (!preserveMemberApp) + await adoptOrganizationSlackMemberApp( + tx, + context.organizationId, + identity.appId, + identity.teamId, + attempt.clientId + ) + if (attempt.sharedApp && !preserveMemberApp) await configureSharedSlackMemberApp(tx, { organizationId: context.organizationId, userId: principal.userId, @@ -537,6 +592,11 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ lastEventAt: null, updatedAt: new Date(), } + if (customInstallation) + await tx + .update(slackSearchInstallation) + .set({ enabled: false, revision: generateId(), updatedAt: new Date() }) + .where(eq(slackSearchInstallation.id, customInstallation.id)) if (existing) await tx .update(slackSearchInstallation) @@ -560,6 +620,12 @@ export const completeSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ action: AuditAction.ORGANIZATION_UPDATED, resourceType: AuditResourceType.ORGANIZATION, resourceId: context.organizationId, - metadata: { setting: 'slack-search', connected: true }, + metadata: { + setting: 'slack-search', + connected: true, + ...(context.attempt.customInstallation + ? { previousInstallationId: context.attempt.customInstallation.id } + : {}), + }, }), }) diff --git a/apps/sim/lib/knowledge/application/slack-search/turns.test.ts b/apps/sim/lib/knowledge/application/slack-search/turns.test.ts new file mode 100644 index 00000000000..e022cf091b0 --- /dev/null +++ b/apps/sim/lib/knowledge/application/slack-search/turns.test.ts @@ -0,0 +1,113 @@ +/** @vitest-environment node */ +import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + enqueue: vi.fn(), + sender: vi.fn(), + findChat: vi.fn(), + resolveChat: vi.fn(), +})) +vi.mock('@/lib/core/outbox/service', () => ({ enqueueOutboxEvent: mocks.enqueue })) +vi.mock('@/lib/knowledge/application/slack-search/chat', () => ({ + requireSlackSearchConversationSender: mocks.sender, + findSlackSearchChatRecord: mocks.findChat, + resolveSlackSearchChatRecord: mocks.resolveChat, +})) + +import { persistSlackSearchTurn } from '@/lib/knowledge/application/slack-search/turns' +import { slackSearchConversationKey } from '@/lib/slack-search/conversation' +import type { SlackSearchJob } from '@/lib/slack-search/types' + +const installation = { + id: 'old-installation', + organizationId: 'org1', + enabled: false, + revision: 'switched', + credentialVersion: 'version1', +} +const job: SlackSearchJob = { + installationId: installation.id, + revision: installation.revision, + credentialId: 'credential1', + credentialVersion: installation.credentialVersion, + receivedAt: Date.now(), + redirectAppId: 'ASHARED', + message: { + appId: 'ACUSTOM', + teamId: 'T1', + eventId: 'Ev1', + userId: 'U1', + channelId: 'D1', + messageTs: '1800000000.1', + query: '', + queryTooLong: false, + }, +} +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.findChat.mockResolvedValue(null) +}) + +describe('durable retired-bot replies', () => { + it('persists a handoff without reading or creating private chat history', async () => { + queueTableRows(schemaMock.slackSearchInstallation, [installation]) + queueTableRows(schemaMock.slackSearchTurn, []) + queueTableRows(schemaMock.slackSearchTurn, [{ count: 0 }]) + const id = await persistSlackSearchTurn(job) + expect(dbChainMockFns.values).toHaveBeenCalledWith( + expect.objectContaining({ id, payload: job }) + ) + expect(mocks.enqueue).toHaveBeenCalledWith( + expect.anything(), + 'slack-search.turn', + { turnId: id }, + { id: `slack-search-turn:${id}` } + ) + expect(mocks.sender).not.toHaveBeenCalled() + expect(mocks.findChat).not.toHaveBeenCalled() + expect(mocks.resolveChat).not.toHaveBeenCalled() + }) + + it('returns the existing turn for a duplicate Slack event without another write', async () => { + queueTableRows(schemaMock.slackSearchInstallation, [installation]) + queueTableRows(schemaMock.slackSearchTurn, [ + { + id: 'existing-turn', + conversationKey: slackSearchConversationKey(installation.id, 'D1', '1800000000.1'), + payload: job, + }, + ]) + await expect(persistSlackSearchTurn(job)).resolves.toBe('existing-turn') + expect(dbChainMockFns.values).not.toHaveBeenCalled() + expect(mocks.enqueue).not.toHaveBeenCalled() + }) + + it.each([ + null, + { ...installation, enabled: true }, + { ...installation, revision: 'changed' }, + { ...installation, credentialVersion: 'rotated' }, + ])('rejects removed, re-enabled, or changed old installations: %j', async (current) => { + queueTableRows(schemaMock.slackSearchInstallation, current ? [current] : []) + await expect(persistSlackSearchTurn(job)).rejects.toThrow('binding changed') + expect(mocks.enqueue).not.toHaveBeenCalled() + }) + + it('still rejects regular search turns for disabled installations', async () => { + queueTableRows(schemaMock.slackSearchInstallation, [installation]) + await expect(persistSlackSearchTurn({ ...job, redirectAppId: undefined })).rejects.toThrow( + 'binding changed' + ) + expect(mocks.enqueue).not.toHaveBeenCalled() + }) + + it('retains the per-thread pending limit for handoff replies', async () => { + queueTableRows(schemaMock.slackSearchInstallation, [installation]) + queueTableRows(schemaMock.slackSearchTurn, []) + queueTableRows(schemaMock.slackSearchTurn, [{ count: 20 }]) + await expect(persistSlackSearchTurn(job)).rejects.toThrow('twenty queued questions') + expect(mocks.enqueue).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/knowledge/application/slack-search/turns.ts b/apps/sim/lib/knowledge/application/slack-search/turns.ts index 2c9cf165fa4..4adaf86dbb3 100644 --- a/apps/sim/lib/knowledge/application/slack-search/turns.ts +++ b/apps/sim/lib/knowledge/application/slack-search/turns.ts @@ -40,7 +40,8 @@ export async function persistSlackSearchTurn(job: SlackSearchJob, expectedUserId .for('update') .limit(1) if ( - !installation?.enabled || + !installation || + (job.redirectAppId ? installation.enabled : !installation.enabled) || installation.revision !== job.revision || installation.credentialVersion !== job.credentialVersion ) @@ -77,16 +78,18 @@ export async function persistSlackSearchTurn(job: SlackSearchJob, expectedUserId } if (duplicate && duplicate.conversationKey !== conversationKey) throw new OrchestrationError('forbidden', 'Slack event conversation changed') - if (conversation) await requireSlackSearchConversationSender(tx, conversation) - const chat = !conversation - ? null - : expectedUserId - ? await resolveSlackSearchChatRecord(tx, { - organizationId: installation.organizationId, - userId: expectedUserId, - conversation, - }) - : await findSlackSearchChatRecord(tx, conversation) + if (conversation && !job.redirectAppId) + await requireSlackSearchConversationSender(tx, conversation) + const chat = + !conversation || job.redirectAppId + ? null + : expectedUserId + ? await resolveSlackSearchChatRecord(tx, { + organizationId: installation.organizationId, + userId: expectedUserId, + conversation, + }) + : await findSlackSearchChatRecord(tx, conversation) if ( chat && (chat.organizationId !== installation.organizationId || diff --git a/apps/sim/lib/slack-search/manifest.test.ts b/apps/sim/lib/slack-search/manifest.test.ts index 6f434da4f74..68bf1a3804c 100644 --- a/apps/sim/lib/slack-search/manifest.test.ts +++ b/apps/sim/lib/slack-search/manifest.test.ts @@ -1,6 +1,5 @@ /** @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { OrchestrationError } from '@/lib/core/orchestration/types' import { createSharedSlackSearchManifest, createSlackSearchManifest, @@ -85,10 +84,11 @@ describe('Search app manifest', () => { expect(manifest.features.agent_view).toEqual({ agent_description: 'Search with sources' }) expect(manifest.display_information.name).toBe('Sim Search') }) - it('requires HTTPS before directing the admin to Slack', () => { - expect(() => - createSlackSearchManifest('Sim Search', 'Search', 'http://localhost:3003') - ).toThrow(OrchestrationError) + it('uses the configured origin without blocking local setup', () => { + const manifest = createSlackSearchManifest('Sim Search', 'Search', 'http://localhost:3000') + expect(manifest.settings.event_subscriptions.request_url).toBe( + 'http://localhost:3000/api/webhooks/slack' + ) }) }) diff --git a/apps/sim/lib/slack-search/manifest.ts b/apps/sim/lib/slack-search/manifest.ts index f8135416f95..c8ce9b5bd2e 100644 --- a/apps/sim/lib/slack-search/manifest.ts +++ b/apps/sim/lib/slack-search/manifest.ts @@ -1,4 +1,3 @@ -import { OrchestrationError } from '@/lib/core/orchestration/types' import { SLACK_MANAGED_USER_CONFIGURATION_CALLBACK_PATH, SLACK_MANAGED_USER_ENROLLMENT_CALLBACK_PATH, @@ -20,12 +19,6 @@ export function createSlackSearchManifest( existingUserScopes: readonly string[] = [] ) { const url = new URL(origin) - if (url.protocol !== 'https:') { - throw new OrchestrationError( - 'validation', - 'Slack needs a public HTTPS URL to send messages to Sim. Configure this instance with a public HTTPS app URL, then retry setup. Localhost is not reachable from Slack.' - ) - } const webhookUrl = new URL(SLACK_SEARCH_WEBHOOK_PATH, url).href return { display_information: { name, description }, diff --git a/apps/sim/lib/slack-search/messages.ts b/apps/sim/lib/slack-search/messages.ts index 1093b86454f..9b37b0ca725 100644 --- a/apps/sim/lib/slack-search/messages.ts +++ b/apps/sim/lib/slack-search/messages.ts @@ -2,6 +2,7 @@ import { truncate } from '@sim/utils/string' import type { SlackJsonObject, SlackMessage } from '@/lib/internal/slack/client' import type { KnowledgeSearchItem } from '@/lib/knowledge/application/search' import type { SlackSearchMessage } from '@/lib/slack-search/types' +import { slackSearchThreadTimestamp } from '@/lib/slack-search/types' function sourceUrl(result: KnowledgeSearchItem, organizationId: string, baseUrl: string): string { if (result.sourceUrl) { @@ -39,6 +40,35 @@ export function slackSearchReply( } } +/** The destination is a verified installation, never a URL supplied by a message or model. */ +export function renderSlackSearchRedirect( + message: SlackSearchMessage, + appId: string +): SlackMessage { + const url = new URL('https://slack.com/app_redirect') + url.searchParams.set('app', appId) + url.searchParams.set('team', message.teamId) + const text = 'This bot has moved. Continue your conversation in the Sim Search app.' + return slackSearchReply( + { ...message, threadTs: slackSearchThreadTimestamp(message) }, + `${text} ${url.href}`, + [ + { type: 'section', text: { type: 'plain_text', text } }, + { + type: 'actions', + elements: [ + { + type: 'button', + action_id: 'sim_search.open_replacement', + text: { type: 'plain_text', text: 'Open Sim Search' }, + url: url.href, + }, + ], + }, + ] + ) +} + /** Presents the first five documents, preserving the ranking of their best returned chunks. */ export function renderSlackSearchResults( message: SlackSearchMessage, diff --git a/apps/sim/lib/slack-search/oauth-state.test.ts b/apps/sim/lib/slack-search/oauth-state.test.ts index 3bf69859e8f..a7008d43ebf 100644 --- a/apps/sim/lib/slack-search/oauth-state.test.ts +++ b/apps/sim/lib/slack-search/oauth-state.test.ts @@ -57,6 +57,28 @@ describe('Slack OAuth state', () => { 'already completed' ) }) + it('round-trips the custom installation snapshot in a single-use shared-app attempt', async () => { + const { encryptedClientSecret, encryptedSigningSecret, ...common } = attempt + const transition = { + ...common, + sharedApp: { id: 'ASHARED', revision: 'env-revision' }, + customInstallation: { + id: 'old-installation', + revision: 'old-revision', + credentialId: 'old-credential', + appId: 'ACUSTOM', + teamId: 'T1', + }, + memberApp: { appId: 'ACUSTOM', teamId: 'T1' }, + } + await storeSlackSearchOAuthAttempt(transition) + expect(JSON.parse(redis.set.mock.calls[0][1])).toEqual(transition) + redis.eval.mockResolvedValueOnce(redis.set.mock.calls[0][1]) + await expect(consumeSlackSearchOAuthAttempt('state', principal)).resolves.toEqual(transition) + await expect(consumeSlackSearchOAuthAttempt('state', principal)).rejects.toThrow( + 'already completed' + ) + }) it('rejects an expired attempt even when storage returns it', async () => { redis.eval.mockResolvedValueOnce( JSON.stringify({ ...attempt, createdAt: Date.now() - 601_000 }) diff --git a/apps/sim/lib/slack-search/oauth-state.ts b/apps/sim/lib/slack-search/oauth-state.ts index 7074500722e..27836bc061e 100644 --- a/apps/sim/lib/slack-search/oauth-state.ts +++ b/apps/sim/lib/slack-search/oauth-state.ts @@ -6,6 +6,14 @@ import { getRedisClient } from '@/lib/core/config/redis' import { OrchestrationError } from '@/lib/core/orchestration/types' const TTL_SECONDS = 600 +const installationSnapshotSchema = z.object({ + id: z.string().min(1), + revision: z.string().min(1), + credentialId: z.string().min(1), + appId: z.string().min(1), + teamId: z.string().min(1), + appRevision: z.string().optional(), +}) const attemptSchema = z .object({ userId: z.string().min(1), @@ -17,16 +25,8 @@ const attemptSchema = z clientId: z.string().min(1), redirectUri: z.string().url(), createdAt: z.number(), - installation: z - .object({ - id: z.string(), - revision: z.string(), - credentialId: z.string(), - appId: z.string(), - teamId: z.string(), - appRevision: z.string().optional(), - }) - .optional(), + installation: installationSnapshotSchema.optional(), + customInstallation: installationSnapshotSchema.optional(), }) .and( z.union([ diff --git a/apps/sim/lib/slack-search/shared-app.test.ts b/apps/sim/lib/slack-search/shared-app.test.ts index 9bce83397a7..2563b257915 100644 --- a/apps/sim/lib/slack-search/shared-app.test.ts +++ b/apps/sim/lib/slack-search/shared-app.test.ts @@ -5,6 +5,7 @@ import { queueTableRows, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const m = vi.hoisted(() => ({ + hosted: true, flag: vi.fn(), env: { SLACK_SEARCH_APP_ID: 'A1', @@ -14,6 +15,11 @@ const m = vi.hoisted(() => ({ }, })) vi.mock('@/lib/core/config/env', () => ({ env: m.env })) +vi.mock('@/lib/core/config/env-flags', () => ({ + get isHosted() { + return m.hosted + }, +})) vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: m.flag })) import { @@ -25,6 +31,7 @@ import { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() + m.hosted = true Object.assign(m.env, { SLACK_SEARCH_APP_ID: 'A1', SLACK_SEARCH_CLIENT_ID: 'client', @@ -34,6 +41,17 @@ beforeEach(() => { m.flag.mockResolvedValue(true) }) describe('shared Slack rollout', () => { + it('requires a hosted deployment even when configured and enabled', async () => { + m.hosted = false + await expect(readSharedSlackSearchApp()).resolves.toBeNull() + await expect(requireSlackSearchAppAvailable('A1')).rejects.toThrow('unavailable') + expect(m.flag).not.toHaveBeenCalled() + }) + it('preserves custom bot handling on self-hosted deployments', async () => { + m.hosted = false + queueTableRows(slackApp, [{ kind: 'custom' }]) + await expect(requireSlackSearchAppAvailable('CUSTOM')).resolves.toBeUndefined() + }) it.each([false, true])('requires both flag and configured app (flag=%s)', async (flag) => { m.flag.mockResolvedValue(flag) if (flag) m.env.SLACK_SEARCH_APP_ID = '' diff --git a/apps/sim/lib/slack-search/shared-app.ts b/apps/sim/lib/slack-search/shared-app.ts index 9431b4565ab..277279a5e6c 100644 --- a/apps/sim/lib/slack-search/shared-app.ts +++ b/apps/sim/lib/slack-search/shared-app.ts @@ -1,13 +1,14 @@ import { db } from '@sim/db' import { slackApp, slackSearchInstallation } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' +import { isHosted } from '@/lib/core/config/env-flags' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { OrchestrationError } from '@/lib/core/orchestration/types' import { getSharedSlackSearchAppConfiguration } from '@/lib/slack-search/shared-app-env' /** Called only inside authorized installation/member operations; never returns secrets to a surface. */ export async function readSharedSlackSearchApp() { - if (!(await isFeatureEnabled('slack-search-shared-app'))) return null + if (!isHosted || !(await isFeatureEnabled('slack-search-shared-app'))) return null return getSharedSlackSearchAppConfiguration() } diff --git a/apps/sim/lib/slack-search/types.ts b/apps/sim/lib/slack-search/types.ts index e81c2341038..84bb80d81dc 100644 --- a/apps/sim/lib/slack-search/types.ts +++ b/apps/sim/lib/slack-search/types.ts @@ -40,6 +40,7 @@ export const slackSearchJobSchema = z.object({ credentialId: id, credentialVersion: id, receivedAt: z.number().int().positive(), + redirectAppId: id.optional(), message: slackSearchMessageSchema, }) export type SlackSearchJob = z.infer From b2b9eedc160b9c39b60ff815a8d4212d6c5c604d Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:31:32 -0700 Subject: [PATCH 04/17] fix(tables): use explicit timestamps for expiration (#7689) * fix(tables): use explicit timestamps for expiration * fix(tables): preserve expiration timestamp offsets * fix(tables): preserve expiration precision during calendar edits * fix(tables): use native timestamp validation Co-Authored-By: Claude Opus 5 (1M context) * chore(tables): keep expiration QA notes local * fix(calendar): validate date and time with Zod * fix(docs): keep unreleased expiration hidden Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../cron/cleanup-table-row-ttl/route.test.ts | 26 + .../api/table/[tableId]/columns/route.test.ts | 29 +- .../app/api/table/[tableId]/columns/route.ts | 5 +- .../components/row-modal/row-modal.test.tsx | 44 +- .../components/row-modal/row-modal.tsx | 28 +- .../table-grid/cells/cell-content.tsx | 3 - .../table-grid/cells/cell-render.test.ts | 47 +- .../table-grid/cells/cell-render.tsx | 5 +- .../table-grid/cells/inline-editors.test.ts | 158 ++++- .../table-grid/cells/inline-editors.tsx | 79 +-- .../components/table-grid/data-row.tsx | 7 +- .../components/table-grid/table-grid.tsx | 1 - .../[tableId]/components/timezone-editing.ts | 4 +- .../tables/[tableId]/utils.test.ts | 32 +- .../[workspaceId]/tables/[tableId]/utils.ts | 10 +- .../cleanup-table-row-ttl.integration.test.ts | 635 ++++++++++++++++++ .../background/cleanup-table-row-ttl.test.ts | 250 ++++++- apps/sim/background/cleanup-table-row-ttl.ts | 71 +- .../lib/copilot/generated/tool-catalog-v1.ts | 34 +- .../lib/copilot/generated/tool-schemas-v1.ts | 34 +- apps/sim/lib/core/utils/timezone.test.ts | 65 +- apps/sim/lib/core/utils/timezone.ts | 23 +- .../__tests__/column-type-registry.test.ts | 141 +--- apps/sim/lib/table/__tests__/sql.test.ts | 56 ++ .../lib/table/__tests__/validation.test.ts | 26 + .../lib/table/column-types/comparison-sql.ts | 14 + .../column-types/extension-points.test.ts | 41 +- .../lib/table/column-types/import-coercion.ts | 20 - .../lib/table/column-types/registry.server.ts | 8 +- apps/sim/lib/table/column-types/registry.ts | 15 +- .../lib/table/column-types/timestamp-sql.ts | 12 + apps/sim/lib/table/column-types/ttl.test.ts | 299 ++++----- apps/sim/lib/table/column-types/ttl.ts | 128 +--- apps/sim/lib/table/column-types/types.ts | 30 +- .../sim/lib/table/columns/retype-cell.test.ts | 37 +- apps/sim/lib/table/columns/service.ts | 39 +- apps/sim/lib/table/dates.ts | 12 +- apps/sim/lib/table/import.test.ts | 39 +- apps/sim/lib/table/import.ts | 11 +- .../lib/table/orchestration/import.test.ts | 2 +- apps/sim/lib/table/rows/service.ts | 5 +- apps/sim/lib/table/sql.ts | 39 +- apps/sim/lib/table/ttl-values.ts | 88 +++ apps/sim/lib/table/validation.ts | 16 +- bun.lock | 1 + packages/emcn/package.json | 3 +- .../calendar/calendar-interaction.test.tsx | 46 ++ .../src/components/calendar/calendar.test.ts | 31 +- .../emcn/src/components/calendar/calendar.tsx | 26 +- 49 files changed, 1818 insertions(+), 957 deletions(-) create mode 100644 apps/sim/background/cleanup-table-row-ttl.integration.test.ts create mode 100644 apps/sim/lib/table/column-types/comparison-sql.ts delete mode 100644 apps/sim/lib/table/column-types/import-coercion.ts create mode 100644 apps/sim/lib/table/column-types/timestamp-sql.ts create mode 100644 apps/sim/lib/table/ttl-values.ts create mode 100644 packages/emcn/src/components/calendar/calendar-interaction.test.tsx diff --git a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts index 9e62e4eb1ce..df762fabaf9 100644 --- a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts +++ b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts @@ -129,4 +129,30 @@ describe('table row TTL cleanup route', () => { }) expect(mockGetJobQueue).not.toHaveBeenCalled() }) + + it.each(['initialization', 'enqueue'])( + 'reports a queue %s failure and permits a later retry', + async (stage) => { + if (stage === 'initialization') { + mockGetJobQueue.mockRejectedValueOnce(new Error('queue unavailable')) + } else { + mockEnqueue.mockRejectedValueOnce(new Error('connection lost')) + } + const request = () => + createMockRequest( + 'GET', + undefined, + {}, + 'http://localhost:3000/api/cron/cleanup-table-row-ttl' + ) + const failed = await GET(request()) + expect(failed.status).toBe(500) + await expect(failed.json()).resolves.toEqual({ + error: 'Failed to dispatch table row TTL cleanup', + }) + const retried = await GET(request()) + expect(retried.status).toBe(200) + await expect(retried.json()).resolves.toEqual({ triggered: true, jobId: 'job-ttl-1' }) + } + ) }) diff --git a/apps/sim/app/api/table/[tableId]/columns/route.test.ts b/apps/sim/app/api/table/[tableId]/columns/route.test.ts index 24830309efc..8e382cc69ee 100644 --- a/apps/sim/app/api/table/[tableId]/columns/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/columns/route.test.ts @@ -55,6 +55,13 @@ vi.mock('@/lib/table/wire', () => ({ vi.mock('@/app/api/table/utils', () => ({ accessError: () => new Response('denied', { status: 403 }), checkAccess: mockCheckAccess, + orchestrationErrorResponse: (error: unknown) => + error instanceof OrchestrationError + ? NextResponse.json( + { error: error.message }, + { status: statusForOrchestrationError(error.code) } + ) + : null, orchestrationOutcomeErrorResponse: ( outcome: { error?: string; errorCode?: OrchestrationErrorCode }, fallback: string @@ -73,7 +80,7 @@ import { type OrchestrationErrorCode, statusForOrchestrationError, } from '@/lib/core/orchestration/types' -import { PATCH } from '@/app/api/table/[tableId]/columns/route' +import { PATCH, POST } from '@/app/api/table/[tableId]/columns/route' const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111' @@ -106,6 +113,26 @@ describe('PATCH /api/table/[tableId]/columns — pre-flight guards', () => { mockRenameColumn.mockResolvedValue({ schema: { columns: [] } }) }) + it.each([ + 'Schema validation failed: A table can have at most 1 Expiration column', + 'Expiration columns are not enabled', + ])('returns a validation response when adding a column fails: %s', async (message) => { + mockAddTableColumn.mockRejectedValueOnce(new OrchestrationError('validation', message)) + const response = await POST( + new NextRequest('http://localhost/api/table/t1/columns', { + method: 'POST', + body: JSON.stringify({ + workspaceId: WORKSPACE_ID, + column: { name: 'expires', type: 'ttl' }, + }), + headers: { 'content-type': 'application/json' }, + }), + { params: Promise.resolve({ tableId: 't1' }) } + ) + expect(response.status).toBe(400) + expect(await response.json()).toEqual({ error: message }) + }) + it('rejects a currency code on a non-currency column without renaming first', async () => { const response = await patch({ name: 'renamed', currencyCode: 'USD' }) diff --git a/apps/sim/app/api/table/[tableId]/columns/route.ts b/apps/sim/app/api/table/[tableId]/columns/route.ts index dff45ad6728..57238d3acec 100644 --- a/apps/sim/app/api/table/[tableId]/columns/route.ts +++ b/apps/sim/app/api/table/[tableId]/columns/route.ts @@ -17,6 +17,7 @@ import { normalizeColumn } from '@/lib/table/wire' import { accessError, checkAccess, + orchestrationErrorResponse, orchestrationOutcomeErrorResponse, rootErrorMessage, tableLockErrorResponse, @@ -63,8 +64,8 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Colum }, }) } catch (error) { - const lockError = tableLockErrorResponse(error) - if (lockError) return lockError + const classifiedError = orchestrationErrorResponse(error) + if (classifiedError) return classifiedError if (isZodError(error)) { return validationErrorResponse(error, 'Invalid request data') } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.test.tsx index 1c0a37a21c2..18573ba0f98 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.test.tsx @@ -100,7 +100,7 @@ const table: TableInfo = { const row: TableRow = { id: 'row-1', - data: { expires_at: Date.parse('2026-11-01T08:00:00Z') / 1000 }, + data: { expires_at: '2026-11-01T01:00:00-07:00' }, executions: {}, position: 0, createdAt: '2026-01-01T00:00:00Z', @@ -119,7 +119,7 @@ describe('RowModal expiration editing', () => { mockUpdateRow.mockResolvedValue(undefined) }) - it('waits for the saved timezone, freezes it, and chooses the later repeated hour', async () => { + it('preserves expiration offsets while timezone settings load or change', async () => { mockUseTimezoneState.mockReturnValue({ timezone: 'Asia/Tokyo', status: 'loading' }) const container = document.createElement('div') document.body.appendChild(container) @@ -136,12 +136,9 @@ describe('RowModal expiration editing', () => { ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true act(() => root.render(createElement(RowModal, props))) - expect(container.querySelector('[aria-label="Edit expires_at"]')?.textContent).toBe( - 'Loading timezone…' - ) - expect(container.querySelector('[data-testid="time"]')).toBeNull() + expect(container.querySelector('[data-testid="time"]')?.value).toBe('01:00') expect(container.querySelector('[data-testid="submit"]')?.disabled).toBe( - true + false ) mockUseTimezoneState.mockReturnValue({ @@ -165,7 +162,7 @@ describe('RowModal expiration editing', () => { expect(mockUpdateRow).toHaveBeenCalledWith({ rowId: 'row-1', - data: { expires_at: Date.parse('2026-11-01T09:30:00Z') / 1000 }, + data: { expires_at: '2026-11-01T01:30:00-07:00' }, }) expect(props.onSuccess).toHaveBeenCalledTimes(1) @@ -209,7 +206,7 @@ describe('RowModal expiration editing', () => { container.remove() }) - it('blocks an invalid saved timezone with the plain-text guidance', () => { + it('allows expiration edits even when the saved timezone is invalid', () => { mockUseTimezoneState.mockReturnValue({ timezone: 'America/Los_Angeles', savedTimezone: 'Mars/Olympus', @@ -229,18 +226,11 @@ describe('RowModal expiration editing', () => { act(() => root.render(createElement(RowModal, props))) - const blockedField = container.querySelector( - '[aria-label="Edit expires_at"]' - ) - expect(blockedField?.textContent).toBe(String(row.data.expires_at)) + expect(container.querySelector('[data-testid="time"]')?.value).toBe('01:00') expect(container.querySelector('[data-testid="submit"]')?.disabled).toBe( - true + false ) expect(mockToastError).not.toHaveBeenCalled() - act(() => blockedField?.click()) - expect(mockToastError).toHaveBeenCalledWith( - 'Your saved timezone “Mars/Olympus” is invalid. Update it in Settings → General before editing Date or Expiration cells.' - ) act(() => root.unmount()) container.remove() }) @@ -259,11 +249,19 @@ describe('RowModal expiration editing', () => { schema: { columns: [ { name: 'name', type: 'string' }, + { name: 'starts_at', type: 'date' }, { name: 'expires_at', type: 'ttl' }, ], }, } - const mixedRow = { ...row, data: { name: 'Ada', expires_at: row.data.expires_at } } + const mixedRow = { + ...row, + data: { + name: 'Ada', + expires_at: row.data.expires_at, + starts_at: '2026-09-07T12:00:00-07:00', + }, + } const props = { mode: 'edit' as const, isOpen: true, @@ -276,12 +274,10 @@ describe('RowModal expiration editing', () => { act(() => root.render(createElement(RowModal, props))) const nameInput = container.querySelector('[data-testid="modal-input"]') - const blockedField = container.querySelector( - '[aria-label="Edit expires_at"]' - ) + const blockedField = container.querySelector('[aria-label="Edit starts_at"]') const submit = container.querySelector('[data-testid="submit"]') expect(nameInput?.value).toBe('Ada') - expect(blockedField?.textContent).toBe(String(row.data.expires_at)) + expect(blockedField?.textContent).toBe(mixedRow.data.starts_at) expect(submit?.disabled).toBe(false) act(() => changeInput(nameInput as HTMLInputElement, 'Grace')) @@ -289,7 +285,7 @@ describe('RowModal expiration editing', () => { expect(mockUpdateRow).toHaveBeenCalledWith({ rowId: 'row-1', - data: { name: 'Grace' }, + data: { name: 'Grace', expires_at: row.data.expires_at }, }) expect(props.onSuccess).toHaveBeenCalledTimes(1) expect(mockToastError).not.toHaveBeenCalled() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx index bbab5f353f2..94939e31e28 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal/row-modal.tsx @@ -22,6 +22,7 @@ import { useParams } from 'next/navigation' import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table' import { columnTypeOf } from '@/lib/table/column-types' import { resolveCurrencyCode } from '@/lib/table/currency' +import { todayAtTtlOffset, ttlValueFromPicker, ttlValueToPickerParts } from '@/lib/table/ttl-values' import { getTimezoneEditBlockedMessage } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/timezone-editing' import { type TimezoneState, useTimezoneState } from '@/hooks/queries/general-settings' import { useDeleteTableRow, useDeleteTableRows, useUpdateTableRow } from '@/hooks/queries/tables' @@ -332,7 +333,7 @@ function ColumnField({ column, value, timeZone, onChange }: ColumnFieldProps) { required={column.required} hint={hint} mono - value={formatValueForInput(value, column.type, timeZone)} + value={formatValueForInput(value, column.type)} onChange={onChange} placeholder='{"key": "value"}' rows={4} @@ -340,28 +341,37 @@ function ColumnField({ column, value, timeZone, onChange }: ColumnFieldProps) { ) } - if (definition.editor === 'date') { - const parts = dateValueToLocalParts(formatValueForInput(value, column.type, timeZone)) + if (definition.editor === 'date' || definition.editor === 'offset-date') { + const storedValue = formatValueForInput(value, column.type) + const offsetParts = + definition.editor === 'offset-date' ? ttlValueToPickerParts(storedValue) : null + const parts = offsetParts ?? dateValueToLocalParts(storedValue) + const pickerToday = offsetParts + ? todayAtTtlOffset(offsetParts.offset) + : todayLocalCalendarDate(timeZone) const valueFromParts = (day: string, time: string | null) => - column.type === 'ttl' && time ? `${day}T${time}` : localPartsToDateValue(day, time, timeZone) + offsetParts + ? ttlValueFromPicker(day, time, offsetParts.offset) + : localPartsToDateValue(day, time, timeZone) return (
onChange(valueFromParts(day, parts.time))} placeholder='Select date' className='flex-1' /> - onChange(valueFromParts(parts.day ?? todayLocalCalendarDate(timeZone), time)) - } + onChange={(time) => onChange(valueFromParts(parts.day ?? pickerToday, time))} placeholder='Add time' className='w-[110px]' /> + {offsetParts && ( + {offsetParts.offset} + )}
) @@ -387,7 +397,7 @@ function ColumnField({ column, value, timeZone, onChange }: ColumnFieldProps) { inputType={ definition.inputMode === 'decimal' && !definition.acceptsFormattedInput ? 'number' : 'text' } - value={formatValueForInput(value, column.type, timeZone)} + value={formatValueForInput(value, column.type)} onChange={onChange} placeholder={`Enter ${column.name}`} /> 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 bdb533d9773..53af9e2482c 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 @@ -14,7 +14,6 @@ interface CellContentProps { /** 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 - timeZone: string timezoneStatus: TimezoneState['status'] isEditing: boolean initialCharacter?: string | null @@ -41,7 +40,6 @@ export function CellContent({ exec, column, workspaceId, - timeZone, timezoneStatus, isEditing, initialCharacter, @@ -57,7 +55,6 @@ export function CellContent({ waitingOnLabels, isEnrichmentOutput, currentWorkspaceId: workspaceId, - timeZone, timezoneStatus, }) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.test.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.test.ts index ab0789ff2d4..529edc3350e 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.test.ts @@ -23,43 +23,23 @@ function column(type: DisplayColumn['type']): DisplayColumn { } describe('resolveCellRender', () => { - it('renders TTL epoch seconds through the date presentation', () => { - expect( - resolveCellRender({ - value: 1_700_000_000, + it.each(['ready', 'loading', 'invalid', 'error'] as const)( + 'renders TTL as the exact UTC string when timezone status is %s', + (timezoneStatus) => { + const value = '2026-06-15T09:00:30Z' + const kind = resolveCellRender({ + value, exec: undefined, column: column('ttl'), waitingOnLabels: undefined, - timeZone: 'America/New_York', + timezoneStatus, }) - ).toEqual({ kind: 'date', text: '2023-11-14T17:13:20-05:00' }) - }) - - it('renders raw epoch seconds when the saved timezone is invalid', () => { - expect( - resolveCellRender({ - value: 1_700_000_000, - exec: undefined, - column: column('ttl'), - waitingOnLabels: undefined, - timeZone: 'America/Los_Angeles', - timezoneStatus: 'invalid', - }) - ).toEqual({ kind: 'date', text: '1700000000', raw: true }) - }) - - it('renders raw epoch seconds while timezone settings are loading', () => { - expect( - resolveCellRender({ - value: 1_700_000_000, - exec: undefined, - column: column('ttl'), - waitingOnLabels: undefined, - timeZone: 'America/Los_Angeles', - timezoneStatus: 'loading', - }) - ).toEqual({ kind: 'date', text: '1700000000', raw: true }) - }) + expect(kind).toEqual({ kind: 'text', text: value }) + expect(renderToStaticMarkup(createElement(CellRender, { kind, isEditing: false }))).toContain( + value + ) + } + ) it('renders the exact stored Date value when timezone settings are unavailable', () => { const stored = '2026-01-15T09:00:00-05:00' @@ -68,7 +48,6 @@ describe('resolveCellRender', () => { exec: undefined, column: column('date'), waitingOnLabels: undefined, - timeZone: 'America/Los_Angeles', timezoneStatus: 'error', }) expect(kind).toEqual({ kind: 'date', text: stored, raw: true }) 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 22971e399d0..6e9045f2bd4 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 @@ -54,8 +54,6 @@ interface ResolveCellRenderInput { /** 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 - /** Effective viewer timezone for instant-like column presentations. */ - timeZone?: string /** Invalid or unavailable preferences render time-based values without conversion. */ timezoneStatus?: TimezoneState['status'] } @@ -67,7 +65,6 @@ export function resolveCellRender({ waitingOnLabels, isEnrichmentOutput, currentWorkspaceId, - timeZone, timezoneStatus, }: ResolveCellRenderInput): CellRenderKind { const isNull = value === null || value === undefined @@ -149,7 +146,7 @@ export function resolveCellRender({ if (timezoneStatus !== undefined && timezoneStatus !== 'ready') { return { kind: 'date', text: stringifyValue(value), raw: true } } - return { kind: 'date', text: definition.formatForInput(value, column, { timezone: timeZone }) } + return { kind: 'date', text: definition.formatForInput(value, column) } } if (column.type === 'string') { const text = stringifyValue(value) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.test.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.test.ts index 0439e39fc75..d08d7cb5a9e 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.test.ts @@ -5,14 +5,23 @@ import { act, createElement, type ReactNode } from 'react' import { createRoot } from 'react-dom/client' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ColumnDefinition } from '@/lib/table' +import { TTL_FORMAT_ERROR } from '@/lib/table/ttl-values' import { dateEditorRawValue, InlineEditor, } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors' import { cleanCellValue } from '@/app/workspace/[workspaceId]/tables/[tableId]/utils' -const { mockToastError, mockUseTimezoneState } = vi.hoisted(() => ({ +const { mockToastError, mockUseTimezoneState, mockCalendar } = vi.hoisted(() => ({ mockToastError: vi.fn(), + mockCalendar: vi.fn( + (_props: { + onChange: (value: string) => void + value?: string + timeLabel?: string + today?: string + }) => null + ), mockUseTimezoneState: vi.fn(), })) @@ -20,7 +29,7 @@ vi.mock('@/hooks/queries/general-settings', () => ({ useTimezoneState: mockUseTi vi.mock('@sim/emcn', () => { const passthrough = ({ children }: { children?: ReactNode }) => children ?? null return { - Calendar: () => null, + Calendar: mockCalendar, cn: (...classes: unknown[]) => classes.filter(Boolean).join(' '), DropdownMenu: passthrough, DropdownMenuContent: passthrough, @@ -56,26 +65,109 @@ describe('dateEditorRawValue', () => { const repeatedRaw = dateEditorRawValue(repeatedWallClock, ttlColumn, timezone) expect(repeatedRaw).toBe(repeatedWallClock) - expect(cleanCellValue(repeatedRaw, ttlColumn, timezone)).toBe( - Date.parse('2026-11-01T06:30:00Z') / 1000 - ) + expect(cleanCellValue(repeatedRaw, ttlColumn, timezone)).toBeNull() const fractionalRaw = dateEditorRawValue('2023-11-14t22:13:20.001Z', ttlColumn, timezone) - expect(cleanCellValue(fractionalRaw, ttlColumn, timezone)).toBe(1_700_000_001) + expect(cleanCellValue(fractionalRaw, ttlColumn, timezone)).toBe('2023-11-14T22:13:20.001-00:00') }) + it.each([ + ['2026-11-01T01:30', '2026-11-01T01:30:00-00:00'], + ['2026-03-08T02:30:45', '2026-03-08T02:30:45-00:00'], + ['2026-09-07', '2026-09-07T00:00:00-00:00'], + ])('saves new picker selections with a zero offset %s', (picked, expected) => { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + const onSave = vi.fn() + act(() => + root.render( + createElement(InlineEditor, { + column: column('ttl'), + value: null, + onSave, + onCancel: vi.fn(), + }) + ) + ) + const picker = mockCalendar.mock.calls.at(-1)![0] + act(() => picker.onChange(picked)) + if (picked.includes('T')) { + const input = container.querySelector('input') as HTMLInputElement + expect(input.value).toBe(expected) + act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) + } + expect(onSave).toHaveBeenCalledWith(expected, 'enter') + expect(mockUseTimezoneState).not.toHaveBeenCalled() + act(() => root.unmount()) + container.remove() + }) + + it.each(['-07:00', '-08:00', '+05:45', '-00:00', '+00:00'])( + 'retains %s when changing the date and time in the picker', + (offset) => { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + const onSave = vi.fn() + act(() => + root.render( + createElement(InlineEditor, { + column: column('ttl'), + value: `2026-09-07T07:30:00.123456${offset}`, + onSave, + onCancel: vi.fn(), + }) + ) + ) + const picker = mockCalendar.mock.calls.at(-1)![0] + expect(picker.timeLabel).toBe(`Time (${offset})`) + act(() => picker.onChange('2026-11-01T01:30:45')) + const input = container.querySelector('input') as HTMLInputElement + expect(input.value).toBe(`2026-11-01T01:30:45${offset}`) + act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) + expect(onSave).toHaveBeenCalledWith(`2026-11-01T01:30:45${offset}`, 'enter') + act(() => root.unmount()) + container.remove() + } + ) + it('keeps ordinary date drafts on their existing display parser', () => { expect(dateEditorRawValue('11/01/2026 1:30:00 AM', column('date'), 'America/New_York')).toBe( '2026-11-01T01:30:00-04:00' ) }) - it('keeps an open TTL edit in its starting timezone when the setting changes', () => { + it('preserves a typed offset timestamp and its microseconds', () => { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + const onSave = vi.fn() + act(() => + root.render( + createElement(InlineEditor, { + column: column('ttl'), + value: null, + onSave, + onCancel: vi.fn(), + }) + ) + ) + const input = container.querySelector('input') as HTMLInputElement + act(() => changeInput(input, '2026-09-07T07:30:00.123456-07:00')) + act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) + expect(onSave).toHaveBeenCalledWith('2026-09-07T07:30:00.123456-07:00', 'enter') + expect(mockToastError).not.toHaveBeenCalled() + act(() => root.unmount()) + container.remove() + }) + + it('keeps an open TTL edit in its supplied offset when the timezone setting changes', () => { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) const onSave = vi.fn() - const value = Date.parse('2026-06-15T13:00:30Z') / 1000 + const value = '2026-06-15T06:00:30-07:00' const props = { value, column: column('ttl'), @@ -92,18 +184,18 @@ describe('dateEditorRawValue', () => { act(() => root.render(createElement(InlineEditor, props))) const input = container.querySelector('input') as HTMLInputElement - expect(input?.value).toBe('06/15/2026 6:00:30 AM') - act(() => changeInput(input, '09/01/2026 9:00 AM')) + expect(input?.value).toBe('2026-06-15T06:00:30-07:00') + act(() => changeInput(input, '2026-09-01T09:00:00Z')) act(() => { input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) }) - expect(onSave).toHaveBeenCalledWith(Date.parse('2026-09-01T16:00:00Z') / 1000, 'enter') + expect(onSave).toHaveBeenCalledWith('2026-09-01T09:00:00-00:00', 'enter') act(() => root.unmount()) container.remove() }) - it('waits for the saved timezone before creating a TTL draft', () => { + it('converts a legacy Z value to a zero-offset draft while timezone settings are loading', () => { mockUseTimezoneState.mockReturnValue({ timezone: 'Asia/Tokyo', status: 'loading', @@ -113,7 +205,7 @@ describe('dateEditorRawValue', () => { const root = createRoot(container) const onSave = vi.fn() const props = { - value: Date.parse('2026-06-15T13:00:30Z') / 1000, + value: '2026-06-15T13:00:30Z', column: column('ttl'), onSave, onCancel: vi.fn(), @@ -121,8 +213,8 @@ describe('dateEditorRawValue', () => { act(() => root.render(createElement(InlineEditor, props))) - expect(container.querySelector('input')).toBeNull() - expect(container.querySelector('[role="status"]')?.textContent).toBe('Loading timezone…') + expect(container.querySelector('input')?.value).toBe('2026-06-15T13:00:30-00:00') + expect(container.querySelector('[role="status"]')).toBeNull() mockUseTimezoneState.mockReturnValue({ timezone: 'America/Los_Angeles', @@ -132,10 +224,10 @@ describe('dateEditorRawValue', () => { const input = container.querySelector('input') as HTMLInputElement expect(input.disabled).toBe(false) - act(() => changeInput(input, '09/01/2026 9:00 AM')) + act(() => changeInput(input, '2026-09-01T09:00:00Z')) act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) - expect(onSave).toHaveBeenCalledWith(Date.parse('2026-09-01T16:00:00Z') / 1000, 'enter') + expect(onSave).toHaveBeenCalledWith('2026-09-01T09:00:00-00:00', 'enter') act(() => root.unmount()) container.remove() }) @@ -185,7 +277,7 @@ describe('dateEditorRawValue', () => { act(() => root.render( createElement(InlineEditor, { - value: Date.parse('2026-06-15T13:00:30Z') / 1000, + value: '2026-06-15T13:00:30Z', column: column('ttl'), onSave, onCancel: vi.fn(), @@ -198,19 +290,28 @@ describe('dateEditorRawValue', () => { act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) expect(onSave).not.toHaveBeenCalled() - expect(mockToastError).toHaveBeenCalledWith('Invalid expiration date') + expect(mockToastError).toHaveBeenCalledWith(TTL_FORMAT_ERROR) act(() => root.unmount()) container.remove() }) it.each([ - { caseName: 'a historical sub-minute offset', timezone: 'Africa/Monrovia', value: 2670 }, + { + caseName: 'a historical sub-minute offset', + timezone: 'Africa/Monrovia', + value: '1970-01-01T00:44:30-00:00', + }, + { + caseName: 'microsecond precision', + timezone: 'America/Los_Angeles', + value: '2026-09-07T07:30:00.123456-07:00', + }, { caseName: 'the far-future representable boundary', timezone: 'Asia/Tokyo', - value: 253_402_300_799, + value: '9999-12-31T23:59:59+00:00', }, - ])('preserves the exact epoch for $caseName when untouched', ({ timezone, value }) => { + ])('preserves the exact offset string for $caseName when untouched', ({ timezone, value }) => { mockUseTimezoneState.mockReturnValue({ timezone, status: 'ready' }) const container = document.createElement('div') document.body.appendChild(container) @@ -236,7 +337,7 @@ describe('dateEditorRawValue', () => { container.remove() }) - it('cancels TTL editing when the saved timezone cannot be loaded', () => { + it('allows TTL editing when the saved timezone cannot be loaded', () => { mockUseTimezoneState.mockReturnValue({ timezone: 'America/Los_Angeles', status: 'error', @@ -249,7 +350,7 @@ describe('dateEditorRawValue', () => { act(() => root.render( createElement(InlineEditor, { - value: 2670, + value: '1970-01-01T00:44:30-00:00', column: column('ttl'), onSave: vi.fn(), onCancel, @@ -257,10 +358,9 @@ describe('dateEditorRawValue', () => { ) ) - expect(onCancel).toHaveBeenCalledOnce() - expect(mockToastError).toHaveBeenCalledWith( - 'We couldn’t load your timezone setting. Try again before editing Date or Expiration cells.' - ) + expect(container.querySelector('input')?.value).toBe('1970-01-01T00:44:30-00:00') + expect(onCancel).not.toHaveBeenCalled() + expect(mockToastError).not.toHaveBeenCalled() act(() => root.unmount()) container.remove() }) @@ -290,7 +390,7 @@ describe('dateEditorRawValue', () => { expect(container.querySelector('input')).toBeNull() expect(onCancel).toHaveBeenCalledOnce() expect(mockToastError).toHaveBeenCalledWith( - 'Your saved timezone “Mars/Olympus” is invalid. Update it in Settings → General before editing Date or Expiration cells.' + 'Your saved timezone “Mars/Olympus” is invalid. Update it in Settings → General before editing Date cells.' ) act(() => root.unmount()) container.remove() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx index fc5779089b8..d6e754beb4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx @@ -17,6 +17,7 @@ import { Check } from '@sim/emcn/icons' import type { ColumnDefinition } from '@/lib/table' import { columnTypeOf } from '@/lib/table/column-types' import { isCalendarDateString } from '@/lib/table/dates' +import { todayAtTtlOffset, ttlValueFromPicker, ttlValueToPickerParts } from '@/lib/table/ttl-values' import { getTimezoneEditBlockedMessage } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/timezone-editing' import { useTimezoneState } from '@/hooks/queries/general-settings' import type { SaveReason } from '../../../types' @@ -120,11 +121,14 @@ function ReadyInlineDateEditor({ const editTimeZoneRef = useRef(initialTimeZone) const timeZone = editTimeZoneRef.current - const storedValue = formatValueForInput(value, column.type, timeZone) + const isOffsetDate = columnTypeOf(column).editor === 'offset-date' + const storedValue = formatValueForInput(value, column.type) const initialDraft = initialCharacter !== undefined ? initialCharacter - : storageToDisplay(storedValue, { seconds: true }) + : isOffsetDate + ? storedValue + : storageToDisplay(storedValue, { seconds: true }) const [draft, setDraft] = useState(initialDraft) const [invalid, setInvalid] = useState(false) /** Picker commits mutate the draft from timeouts/child handlers; reading it @@ -132,9 +136,9 @@ function ReadyInlineDateEditor({ const draftRef = useRef(draft) draftRef.current = draft - /** The calendar works on wall times; feed it the draft's literal wall - * representation. */ - const draftParts = dateValueToLocalParts(displayToStorage(draft, timeZone) ?? storedValue) + const offsetParts = isOffsetDate ? ttlValueToPickerParts(draft) : null + const draftParts = + offsetParts ?? dateValueToLocalParts(displayToStorage(draft, timeZone) ?? storedValue) const pickerValue = draftParts.day ? draftParts.time ? `${draftParts.day}T${draftParts.time}` @@ -160,19 +164,11 @@ function ReadyInlineDateEditor({ if (doneRef.current) return clearTimeout(blurTimeoutRef.current) const current = draftRef.current - // Untouched draft → re-save the stored value byte-identical. Re-parsing - // the display form would re-stamp the offset with THIS viewer's zone, - // silently shifting the instant of a value someone else wrote. + /** Preserve Date cells' stored offsets instead of reinterpreting their + * display text in the viewer's timezone. */ if (storageVal === undefined && initialCharacter === undefined && current === initialDraft) { doneRef.current = true - onSave( - column.type === 'ttl' - ? (value ?? null) - : storedValue - ? cleanCellValue(storedValue, column, timeZone) - : null, - reason - ) + onSave(storedValue ? cleanCellValue(storedValue, column, timeZone) : null, reason) return } const raw = dateEditorRawValue(current, column, timeZone, storageVal) @@ -193,17 +189,7 @@ function ReadyInlineDateEditor({ doneRef.current = true onSave(cleaned, reason) }, - [ - invalid, - onSave, - onCancel, - timeZone, - initialDraft, - initialCharacter, - storedValue, - column, - value, - ] + [invalid, onSave, onCancel, timeZone, initialDraft, initialCharacter, storedValue, column] ) const handleKeyDown = useCallback( @@ -249,21 +235,25 @@ function ReadyInlineDateEditor({ * immediately) or a local `YYYY-MM-DDTHH:mm[:ss]` wall time (update the * draft and keep editing). */ - const handlePickerChange = useCallback( - (picked: string) => { - clearTimeout(blurTimeoutRef.current) - if (isCalendarDateString(picked)) { - doSave('enter', picked) - return - } - const canonical = displayToStorage(picked, timeZone) - if (!canonical) return - setDraft(storageToDisplay(canonical, { seconds: true })) + const handlePickerChange = (picked: string) => { + clearTimeout(blurTimeoutRef.current) + if (isCalendarDateString(picked)) { + doSave('enter', offsetParts ? ttlValueFromPicker(picked, null, offsetParts.offset) : picked) + return + } + if (offsetParts) { + const [day, time] = picked.split('T') + setDraft(ttlValueFromPicker(day, time ?? null, offsetParts.offset)) setInvalid(false) inputRef.current?.focus() - }, - [doSave, timeZone] - ) + return + } + const canonical = displayToStorage(picked, timeZone) + if (!canonical) return + setDraft(storageToDisplay(canonical, { seconds: true })) + setInvalid(false) + inputRef.current?.focus() + } const handlePickerOpenChange = useCallback((open: boolean) => { if (!open && !doneRef.current) { @@ -284,7 +274,7 @@ function ReadyInlineDateEditor({ }} onKeyDown={handleKeyDown} onBlur={scheduleBlurSave} - placeholder='mm/dd/yyyy' + placeholder={isOffsetDate ? 'YYYY-MM-DDTHH:mm:ss±HH:mm' : 'mm/dd/yyyy'} className={cn( 'w-full min-w-0 select-text border-none bg-transparent p-0 text-[var(--text-primary)] text-small outline-hidden', invalid && 'text-[var(--text-error)]' @@ -304,7 +294,10 @@ function ReadyInlineDateEditor({ value={pickerValue} onChange={handlePickerChange} showTime - today={todayLocalCalendarDate(timeZone)} + timeLabel={offsetParts ? `Time (${offsetParts.offset})` : undefined} + today={ + offsetParts ? todayAtTtlOffset(offsetParts.offset) : todayLocalCalendarDate(timeZone) + } /> @@ -503,6 +496,8 @@ export function InlineEditor(props: InlineEditorProps) { switch (columnTypeOf(props.column).editor) { case 'date': return + case 'offset-date': + return case 'select': return // `toggle` types never open an editor — the grid flips them in place — so 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 51b38ab318d..86963aa768b 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 @@ -27,9 +27,7 @@ export interface DataRowProps { /** Current workspace id — forwarded to cells so in-workspace resource URLs * render as tagged-resource chips. */ workspaceId: string - /** Effective viewer timezone used to render TTL instants. */ - timeZone: string - /** Whether Date and Expiration values can be formatted and edited safely. */ + /** Whether Date values can be formatted and edited safely. */ timezoneStatus: TimezoneState['status'] rowIndex: number isFirstRow: boolean @@ -119,7 +117,6 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { prev.row !== next.row || prev.columns !== next.columns || prev.workspaceId !== next.workspaceId || - prev.timeZone !== next.timeZone || prev.timezoneStatus !== next.timezoneStatus || prev.rowIndex !== next.rowIndex || prev.isFirstRow !== next.isFirstRow || @@ -168,7 +165,6 @@ export const DataRow = React.memo(function DataRow({ row, columns, workspaceId, - timeZone, timezoneStatus, rowIndex, isFirstRow, @@ -405,7 +401,6 @@ export const DataRow = React.memo(function DataRow({
{ expect(formatValueForInput('2026-07-06', 'date')).toBe('2026-07-06') }) - it('renders TTL instants in the editor timezone without changing the instant', () => { - expect(formatValueForInput(1_700_000_000, 'ttl', 'America/New_York')).toBe( - '2023-11-14T17:13:20-05:00' - ) - expect( - cleanCellValue('2023-11-14 17:13:20', { name: 'expires_at', type: 'ttl' }, 'America/New_York') - ).toBe(1_700_000_000) - expect( - cleanCellValue('2023-11-14', { name: 'expires_at', type: 'ttl' }, 'America/New_York') - ).toBe(1_699_938_000) - }) - - it('uses the latest effective timezone for each TTL edit', () => { + it('preserves TTL strings independently of the viewer timezone', () => { const column = { name: 'expires_at', type: 'ttl' } as const - const input = '2026-06-15 09:00:30' - - expect(cleanCellValue(input, column, 'America/New_York')).toBe( - Date.parse('2026-06-15T13:00:30Z') / 1000 - ) - expect(cleanCellValue(input, column, 'Asia/Kathmandu')).toBe( - Date.parse('2026-06-15T03:15:30Z') / 1000 - ) - expect(cleanCellValue(input, column, 'America/New_York')).toBe( - Date.parse('2026-06-15T13:00:30Z') / 1000 - ) + const input = '2026-06-15T09:00:30Z' + for (const timezone of ['UTC', 'America/New_York', 'Asia/Kathmandu', 'Mars/Olympus']) { + expect(formatValueForInput(input, 'ttl')).toBe('2026-06-15T09:00:30-00:00') + expect(cleanCellValue(input, column, timezone)).toBe('2026-06-15T09:00:30-00:00') + expect(cleanCellValue('2026-06-15 09:00:30', column, timezone)).toBeNull() + expect(cleanCellValue(1_700_000_000, column, timezone)).toBeNull() + } }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts index c9892f8466e..30e1062d482 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts @@ -56,7 +56,7 @@ export function cleanCellValue( // Everything else runs the SAME coercion the server will run, so the // optimistic cache holds exactly the value that gets persisted. const columnType = columnTypeOf(column) - const coerced = columnType.coerce(value as JsonValue, column, { timezone: timeZone }) + const coerced = columnType.coerce(value as JsonValue, column) if (coerced.ok) return coerced.value const salvaged = columnType.salvage?.(value as JsonValue, column) return salvaged?.ok ? salvaged.value : null @@ -69,7 +69,7 @@ export function cleanCellValue( * row data already has the new mapping's value) would otherwise render * `[object Object]` via `String(value)`. */ -export function formatValueForInput(value: unknown, type: string, timeZone?: string): string { +export function formatValueForInput(value: unknown, type: string): string { if (value === null || value === undefined) return '' const definition = columnTypeById(type) // Shape-drift guard, kept ahead of the registry: a column whose declared type @@ -79,11 +79,7 @@ export function formatValueForInput(value: unknown, type: string, timeZone?: str if (typeof value === 'object' && !definition.storesOpaqueIds && type !== 'json') { return JSON.stringify(value) } - return definition.formatForInput( - value, - { name: '', type: type as ColumnType }, - { timezone: timeZone } - ) + return definition.formatForInput(value, { name: '', type: type as ColumnType }) } /** A canonical date-cell value split into its wall-clock editing parts. */ diff --git a/apps/sim/background/cleanup-table-row-ttl.integration.test.ts b/apps/sim/background/cleanup-table-row-ttl.integration.test.ts new file mode 100644 index 00000000000..c7b210ec0eb --- /dev/null +++ b/apps/sim/background/cleanup-table-row-ttl.integration.test.ts @@ -0,0 +1,635 @@ +/** + * @vitest-environment node + * + * Destructive integration tests against a migrated, disposable local expiration_qa database. + * Set both DATABASE_URL and TABLE_TTL_TEST_DATABASE_URL to that database. All worker SQL, + * table transactions, schema reads, and row-count triggers run for real. + */ +import { writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { sql } from 'drizzle-orm' +import postgres from 'postgres' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.unmock('@sim/db') +vi.unmock('@sim/db/schema') +vi.unmock('drizzle-orm') + +const { enabled, signalChanged, fireTrigger } = vi.hoisted(() => ({ + enabled: vi.fn(), + signalChanged: vi.fn(), + fireTrigger: vi.fn(), +})) +vi.mock('@/lib/table/ttl-availability', () => ({ isTableRowTtlEnabled: enabled })) +vi.mock('@/lib/table/events', () => ({ signalTableRowsChanged: signalChanged })) +vi.mock('@/lib/table/trigger', () => ({ fireTableTrigger: fireTrigger })) + +import { db } from '@sim/db' +import { validatedTimestampSql } from '@/lib/table/column-types/timestamp-sql' +import { updateColumnConstraints } from '@/lib/table/columns/service' +import { getDeleteSnapshotBatchSize } from '@/lib/table/constants' +import { replaceTableRowsWithTx } from '@/lib/table/rows/service' +import { getTableById } from '@/lib/table/service' +import { fieldPredicate } from '@/lib/table/sql' +import { normalizeTtlTimestamp, TTL_TIMESTAMP_VALIDATION } from '@/lib/table/ttl-values' +import type { TableSchema } from '@/lib/table/types' +import { checkBatchUniqueConstraintsDb, coerceRowToSchema } from '@/lib/table/validation' +import { runCleanupTableRowTtl } from '@/background/cleanup-table-row-ttl' + +const url = process.env.TABLE_TTL_TEST_DATABASE_URL +if (url) { + const parsed = new URL(url) + const otherDatabase = Object.entries(process.env).some( + ([key, value]) => /^DATABASE_(URL|REPLICA_URL)(_|$)/.test(key) && value && value !== url + ) + if ( + !['127.0.0.1', 'localhost'].includes(parsed.hostname) || + parsed.pathname !== '/expiration_qa' || + process.env.DATABASE_URL !== url || + otherDatabase + ) { + throw new Error('This suite requires only the disposable local expiration_qa database') + } +} +const control = postgres(url ?? 'postgres://localhost/disabled_expiration_test', { + max: 4, + onnotice: () => {}, +}) +const workspaceId = generateId() +const userId = generateId() +const expired = '2020-01-01T00:00:00Z' +const future = '9998-01-01T00:00:00Z' +const schema = { columns: [{ id: 'expires', name: 'expires_at', type: 'ttl' }] } +const measurements: Record = {} + +async function createTable(columns = schema.columns): Promise { + const id = generateId() + await control`INSERT INTO user_table_definitions (id, workspace_id, name, schema, created_by, max_rows) + VALUES (${id}, ${workspaceId}, ${id}, ${control.json({ columns })}, ${userId}, 2000000)` + return id +} + +async function seedRows(tableId: string, count: number, value: string | null = expired) { + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data, position, created_at) + SELECT ${tableId} || '-' || lpad(n::text, 9, '0'), ${tableId}, ${workspaceId}, + jsonb_build_object('expires', ${value}::text), n, '2020-01-01'::timestamp + FROM generate_series(1, ${count}) AS n` +} + +async function rowCount(tableId: string): Promise { + const [result] = + await control`SELECT count(*)::int AS count FROM user_table_rows WHERE table_id = ${tableId}` + const [definition] = + await control`SELECT row_count FROM user_table_definitions WHERE id = ${tableId}` + expect(definition.row_count).toBe(result.count) + return result.count +} + +async function installDeleteFault(tableId: string, body: string) { + await control.unsafe(`CREATE OR REPLACE FUNCTION expiration_qa_delete_fault() RETURNS trigger + LANGUAGE plpgsql AS $function$ BEGIN + IF OLD.table_id = TG_ARGV[0] THEN ${body} END IF; + RETURN OLD; + END $function$`) + await control.unsafe(`CREATE TRIGGER expiration_qa_delete_fault BEFORE DELETE ON user_table_rows + FOR EACH ROW EXECUTE FUNCTION expiration_qa_delete_fault('${tableId}')`) +} + +async function removeDeleteFault() { + await control`DROP TRIGGER IF EXISTS expiration_qa_delete_fault ON user_table_rows` + await control`DROP FUNCTION IF EXISTS expiration_qa_delete_fault()` +} + +async function waitForSleepingDelete(): Promise { + for (let attempt = 0; attempt < 400; attempt++) { + const rows = await control`SELECT pid FROM pg_stat_activity + WHERE datname = current_database() AND wait_event = 'PgSleep' + AND query LIKE '%WITH locked_rows%'` + if (rows[0]) return Number(rows[0].pid) + await sleep(5) + } + throw new Error('Cleanup never reached the injected in-transaction pause') +} + +describe.skipIf(!url)('Expiration with real PostgreSQL transactions', () => { + beforeAll(async () => { + await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) + VALUES (${userId}, 'Expiration integration fixture', ${`${userId}@example.test`}, true, now(), now())` + await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) + VALUES (${workspaceId}, 'Expiration integration fixtures', ${userId}, ${userId})` + }) + + beforeEach(async () => { + vi.clearAllMocks() + enabled.mockResolvedValue(true) + fireTrigger.mockResolvedValue(undefined) + await control`DELETE FROM user_table_definitions WHERE workspace_id = ${workspaceId}` + }) + + afterEach(async () => { + await removeDeleteFault() + vi.restoreAllMocks() + }) + + afterAll(async () => { + await control`DELETE FROM workspace WHERE id = ${workspaceId}` + await control`DELETE FROM "user" WHERE id = ${userId}` + writeFileSync( + join(tmpdir(), 'expiration-qa-measurements.json'), + JSON.stringify(measurements, null, 2) + ) + await control.end() + }) + + it('does nothing with no TTL, empty tables, missing/null/invalid cells, or only future deadlines', async () => { + const plain = await createTable([{ id: 'expires', name: 'expires_at', type: 'date' }]) + await seedRows(plain, 1) + await createTable() + const table = await createTable() + await seedRows(table, 1, future) + for (const [index, value] of [ + null, + '', + 'not-a-date', + '2026-02-30T00:00:00Z', + 0, + {}, + [], + ].entries()) { + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json(index === 0 ? {} : { expires: value })})` + } + const result = await runCleanupTableRowTtl() + expect(result.deleted).toBe(0) + expect(await rowCount(plain)).toBe(1) + expect(await rowCount(table)).toBe(8) + expect(fireTrigger).not.toHaveBeenCalled() + }) + + it('deletes exactly through the cutoff and preserves a future microsecond across offsets', async () => { + vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-09-07T12:00:00.500Z')) + const table = await createTable() + const values = [ + '2026-09-07T12:00:00.499999Z', + '2026-09-07T12:00:00.500000Z', + '2026-09-07T05:00:00.500000-07:00', + '2026-09-07T17:45:00.500000+05:45', + '2026-09-07T12:00:00.500001Z', + '2026-09-07T05:00:00.500001-07:00', + ] + for (const value of values) { + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json({ expires: value })})` + } + expect((await runCleanupTableRowTtl()).deleted).toBe(4) + expect(await rowCount(table)).toBe(2) + expect(fireTrigger.mock.calls[0][4]).toHaveLength(4) + }) + + it('respects feature disablement, delete locks, and archival, then catches up when restored', async () => { + const table = await createTable() + await seedRows(table, 1) + enabled.mockResolvedValue(false) + expect((await runCleanupTableRowTtl()).deleted).toBe(0) + enabled.mockResolvedValue(true) + await control`UPDATE user_table_definitions SET delete_locked = true WHERE id = ${table}` + expect((await runCleanupTableRowTtl()).deleted).toBe(0) + await control`UPDATE user_table_definitions SET delete_locked = false, archived_at = now() WHERE id = ${table}` + expect((await runCleanupTableRowTtl()).deleted).toBe(0) + expect(await rowCount(table)).toBe(1) + await control`UPDATE user_table_definitions SET archived_at = null WHERE id = ${table}` + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + }) + + it('hits the real 100-batch limit and deletes the exact remaining row on the next pass', async () => { + const table = await createTable() + const capacity = 100 * getDeleteSnapshotBatchSize() + await seedRows(table, capacity + 1) + expect(await runCleanupTableRowTtl()).toEqual({ + batches: 100, + deleted: capacity, + limitReached: true, + }) + expect(await rowCount(table)).toBe(1) + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + expect(await rowCount(table)).toBe(0) + measurements.singleRunCapacity = capacity + }, 30000) + + it('services more than 100 tables across passes without losing the unselected table', async () => { + const tables: string[] = [] + for (let index = 0; index < 101; index++) { + const table = await createTable() + tables.push(table) + await seedRows(table, 1) + } + const first = await runCleanupTableRowTtl() + expect(first).toEqual({ batches: 100, deleted: 100, limitReached: true }) + const remaining = + await control`SELECT count(*)::int AS count FROM user_table_rows WHERE workspace_id = ${workspaceId}` + expect(remaining[0].count).toBe(1) + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + expect(await rowCount(tables[0])).toBe(0) + measurements.tableLimit = { tables: 101, firstPassDeleted: first.deleted, secondPassDeleted: 1 } + }, 30000) + + it('revisits a skipped locked row on the next pass', async () => { + const table = await createTable() + await seedRows(table, 2) + const locked = Promise.withResolvers() + const release = Promise.withResolvers() + const holding = control.begin(async (trx) => { + await trx`SELECT id FROM user_table_rows WHERE table_id = ${table} ORDER BY id LIMIT 1 FOR UPDATE` + locked.resolve() + await release.promise + }) + await locked.promise + try { + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + } finally { + release.resolve() + await holding + } + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + expect(await rowCount(table)).toBe(0) + }) + + it('drains 1001 expiring tables across bounded passes', async () => { + for (let index = 0; index < 1001; index++) { + await seedRows(await createTable(), 1) + } + let deleted = 0 + let passes = 0 + while (deleted < 1001) { + const result = await runCleanupTableRowTtl() + expect(result.batches).toBeLessThanOrEqual(100) + expect(result.deleted).toBeGreaterThan(0) + deleted += result.deleted + expect(++passes).toBeLessThanOrEqual(11) + } + expect(deleted).toBe(1001) + measurements.manyTables = { tables: 1001, passes } + }, 30000) + + it('gives small tables a turn before revisiting a large backlog', async () => { + const large = await createTable() + const batch = getDeleteSnapshotBatchSize() + await seedRows(large, batch * 100) + const small: string[] = [] + for (let index = 0; index < 20; index++) { + const table = await createTable() + small.push(table) + await seedRows(table, 1) + } + await runCleanupTableRowTtl() + for (const table of small) expect(await rowCount(table)).toBe(0) + const order = fireTrigger.mock.calls.map((call) => call[0]) + const firstLarge = order.indexOf(large) + const secondLarge = order.indexOf(large, firstLarge + 1) + for (const table of small) expect(order.indexOf(table)).toBeLessThan(secondLarge) + expect(await rowCount(large)).toBeGreaterThan(0) + }, 30000) + + it.each(['delete lock', 'archive', 'remove column'])( + 'rechecks a mid-run %s before the next batch', + async (change) => { + const table = await createTable() + const batch = getDeleteSnapshotBatchSize() + await seedRows(table, batch + 1) + fireTrigger.mockImplementationOnce(async () => { + if (change === 'delete lock') + await control`UPDATE user_table_definitions SET delete_locked = true WHERE id = ${table}` + if (change === 'archive') + await control`UPDATE user_table_definitions SET archived_at = now() WHERE id = ${table}` + if (change === 'remove column') + await control`UPDATE user_table_definitions SET schema = '{"columns":[]}'::jsonb WHERE id = ${table}` + }) + expect((await runCleanupTableRowTtl()).deleted).toBe(batch) + expect(await rowCount(table)).toBe(1) + await control`UPDATE user_table_definitions SET delete_locked = false, archived_at = null, schema = ${control.json(schema)} WHERE id = ${table}` + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + } + ) + + it('uses one cutoff per run and finds newly expired rows behind its cursor next time', async () => { + const now = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-09-07T12:00:00Z')) + const table = await createTable() + await seedRows(table, getDeleteSnapshotBatchSize()) + const lateId = generateId() + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data, created_at) + VALUES (${lateId}, ${table}, ${workspaceId}, ${control.json({ expires: '2026-09-07T12:00:01Z' })}, '2021-01-01')` + fireTrigger.mockImplementationOnce(async () => { + now.mockReturnValue(Date.parse('2026-09-07T12:00:02Z')) + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data, created_at) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json({ expires: expired })}, '2010-01-01')` + }) + expect((await runCleanupTableRowTtl()).deleted).toBe(getDeleteSnapshotBatchSize()) + expect(await rowCount(table)).toBe(2) + expect((await runCleanupTableRowTtl()).deleted).toBe(2) + }) + + it('handles two concurrent cleanup runs without duplicate deletes or snapshots', async () => { + const table = await createTable() + const count = getDeleteSnapshotBatchSize() * 4 + 1 + await seedRows(table, count) + const results = await Promise.all([runCleanupTableRowTtl(), runCleanupTableRowTtl()]) + expect(results.reduce((sum, result) => sum + result.deleted, 0)).toBe(count) + expect(await rowCount(table)).toBe(0) + const ids = fireTrigger.mock.calls.flatMap((call) => + call[4].map((row: { id: string }) => row.id) + ) + expect(ids).toHaveLength(count) + expect(new Set(ids).size).toBe(count) + }) + + it.each([future, null])( + 'preserves a locked row whose expiration changes to %s', + async (value) => { + const table = await createTable() + await seedRows(table, 1) + const locked = Promise.withResolvers() + const release = Promise.withResolvers() + const holding = control.begin(async (trx) => { + await trx`SELECT id FROM user_table_rows WHERE table_id = ${table} FOR UPDATE` + locked.resolve() + await release.promise + await trx`UPDATE user_table_rows SET data = ${trx.json({ expires: value })} WHERE table_id = ${table}` + }) + await locked.promise + try { + expect((await runCleanupTableRowTtl()).deleted).toBe(0) + } finally { + release.resolve() + await holding + } + expect((await runCleanupTableRowTtl()).deleted).toBe(0) + expect(await rowCount(table)).toBe(1) + } + ) + + it('rolls back a failed batch, keeps prior commits, and drains the remainder after repair', async () => { + const table = await createTable() + const batch = getDeleteSnapshotBatchSize() + await seedRows(table, batch * 2) + await installDeleteFault( + table, + `IF OLD.position > ${batch} THEN RAISE EXCEPTION 'injected expiration failure'; END IF;` + ) + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 2, + deleted: batch, + limitReached: false, + }) + expect(await rowCount(table)).toBe(batch) + expect(signalChanged).toHaveBeenCalledWith(table) + await removeDeleteFault() + expect((await runCleanupTableRowTtl()).deleted).toBe(batch) + expect(await rowCount(table)).toBe(0) + }) + + it('skips a persistently broken first table, drains healthy tables, and retries after repair', async () => { + const cutoff = '2026-09-07T12:00:00.000Z' + vi.spyOn(Date, 'now').mockReturnValue(Date.parse(cutoff)) + await createTable() + await createTable() + const tables = await control<{ id: string }[]>`SELECT id FROM user_table_definitions + WHERE workspace_id = ${workspaceId} ORDER BY md5(id || ${cutoff}), id` + const [broken, healthy] = tables.map(({ id }) => id) + const healthyRows = getDeleteSnapshotBatchSize() + 1 + await seedRows(broken, 3) + await seedRows(healthy, healthyRows) + await installDeleteFault(broken, "RAISE EXCEPTION 'injected table failure';") + + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 4, + deleted: healthyRows, + limitReached: false, + }) + expect(await rowCount(broken)).toBe(3) + expect(await rowCount(healthy)).toBe(0) + expect(signalChanged).toHaveBeenCalledWith(healthy) + expect(signalChanged).not.toHaveBeenCalledWith(broken) + expect(fireTrigger).toHaveBeenCalledTimes(2) + + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 1, + deleted: 0, + limitReached: false, + }) + expect(await rowCount(broken)).toBe(3) + await removeDeleteFault() + expect((await runCleanupTableRowTtl()).deleted).toBe(3) + expect(await rowCount(broken)).toBe(0) + }) + + it('recovers from a real backend connection loss during DELETE without partial deletion', async () => { + const table = await createTable() + await seedRows(table, 3) + await installDeleteFault(table, 'PERFORM pg_sleep(10);') + const deleting = runCleanupTableRowTtl().then( + (result) => ({ result }), + (error: unknown) => ({ error }) + ) + const pid = await waitForSleepingDelete() + await control`SELECT pg_terminate_backend(${pid})` + expect(await deleting).toEqual({ result: { batches: 1, deleted: 0, limitReached: false } }) + expect(await rowCount(table)).toBe(3) + await removeDeleteFault() + expect((await runCleanupTableRowTtl()).deleted).toBe(3) + expect(await rowCount(table)).toBe(0) + }, 15000) + + it('stops between batches on cancellation and restarts without retaining a stale cursor', async () => { + const table = await createTable() + const batch = getDeleteSnapshotBatchSize() + await seedRows(table, batch + 1) + const abort = new AbortController() + fireTrigger.mockImplementationOnce(async () => abort.abort()) + expect((await runCleanupTableRowTtl(abort.signal)).deleted).toBe(batch) + expect(await rowCount(table)).toBe(1) + expect((await runCleanupTableRowTtl()).deleted).toBe(1) + }) + + it('bounds snapshots by bytes and still progresses past an oversized stored row', async () => { + const table = await createTable() + await seedRows(table, 3) + await control`UPDATE user_table_rows SET data = data || jsonb_build_object('wide', repeat('x', 33 * 1024 * 1024)) WHERE table_id = ${table} AND position = 1` + await control`UPDATE user_table_rows SET data = data || jsonb_build_object('wide', repeat('y', 17 * 1024 * 1024)) WHERE table_id = ${table} AND position > 1` + expect((await runCleanupTableRowTtl()).deleted).toBe(3) + expect(fireTrigger.mock.calls.map((call) => call[4].length)).toEqual([1, 1, 1]) + expect(await rowCount(table)).toBe(0) + }, 30000) + + it('matches equivalent instants for equality and membership without casting malformed stored values', async () => { + const table = await createTable() + const values = [ + '2090-09-07T07:30:00.000001-07:00', + '2090-09-07T20:15:00.000001+05:45', + '2090-09-07T14:30:00.000001Z', + '2090-09-07T14:30:00.000001-00:00', + '2090-09-07T14:30:00.000002-00:00', + null, + '', + '2090-02-30T00:00:00Z', + 'not-a-date', + ] + for (const value of values) { + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json({ expires: value })})` + } + const column = { id: 'expires', name: 'expires_at', type: 'ttl' as const } + for (const op of ['eq', 'ne', 'in', 'nin'] as const) { + const instant = '2090-09-07T14:30:00.000001+00:00' + const value = op === 'in' || op === 'nin' ? [instant] : instant + const predicate = fieldPredicate('user_table_rows', 'expires', op, value, column) + const rows = await db.execute(sql`SELECT count(*)::int AS count FROM user_table_rows + WHERE table_id = ${table} AND ${predicate}`) + expect(rows[0].count).toBe(op === 'eq' || op === 'in' ? 4 : 5) + } + const nullPredicate = fieldPredicate('user_table_rows', 'expires', 'eq', null, column) + const rows = await db.execute( + sql`SELECT count(*)::int AS count FROM user_table_rows WHERE table_id = ${table} AND ${nullPredicate}` + ) + expect(rows[0].count).toBe(1) + }) + + it('preserves offsets in storage while enforcing uniqueness by the exact instant', async () => { + const table = await createTable() + const uniqueSchema: TableSchema = { + columns: [{ id: 'expires', name: 'expires_at', type: 'ttl', unique: true }], + } + const first = { expires: '2090-09-07T07:30:00.000001-07:00' } + const equivalent = { expires: '2090-09-07T14:30:00.000001Z' } + const nextMicrosecond = { expires: '2090-09-07T20:15:00.000002+05:45' } + for (const row of [first, equivalent, nextMicrosecond]) { + expect(coerceRowToSchema(row, uniqueSchema, 'reject').valid).toBe(true) + } + expect(first.expires).toBe('2090-09-07T07:30:00.000001-07:00') + expect(equivalent.expires).toBe('2090-09-07T14:30:00.000001-00:00') + expect(nextMicrosecond.expires).toBe('2090-09-07T20:15:00.000002+05:45') + const withinBatch = await checkBatchUniqueConstraintsDb( + table, + [first, equivalent, nextMicrosecond], + uniqueSchema + ) + expect(withinBatch.errors.map(({ row }) => row)).toEqual([1]) + await seedRows(table, 1, first.expires) + const againstStored = await checkBatchUniqueConstraintsDb( + table, + [equivalent, nextMicrosecond], + uniqueSchema + ) + expect(againstStored.errors.map(({ row }) => row)).toEqual([0]) + const definition = await getTableById(table) + expect(definition).not.toBeNull() + await expect( + db.transaction((tx) => + replaceTableRowsWithTx( + tx, + { + tableId: table, + workspaceId, + rows: [first, equivalent], + secretProvenance: undefined, + }, + { ...definition!, schema: uniqueSchema }, + 'offset-qa' + ) + ) + ).rejects.toThrow('must be unique') + expect(await rowCount(table)).toBe(1) + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data, position) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json(equivalent)}, 2)` + await expect( + updateColumnConstraints({ tableId: table, columnName: 'expires', unique: true }, 'offset-qa') + ).rejects.toThrow('duplicate') + await control`UPDATE user_table_rows SET data = ${control.json(nextMicrosecond)} WHERE table_id = ${table} AND position = 2` + const constrained = await updateColumnConstraints( + { tableId: table, columnName: 'expires', unique: true }, + 'offset-qa' + ) + expect(constrained.schema.columns[0].unique).toBe(true) + const stored = + await control`SELECT data->>'expires' AS value FROM user_table_rows WHERE table_id = ${table} ORDER BY position` + expect(stored.map(({ value }) => value)).toEqual([first.expires, nextMicrosecond.expires]) + }) + + it('agrees with PostgreSQL for deterministic offset, leap-year, and precision samples', async () => { + const samples: string[] = [] + for (const year of ['0001', '0099', '1900', '2000', '2024', '2026', '9998']) { + for (const day of ['01-01', '02-28', '03-01', '12-31']) { + for (const offset of [ + 'Z', + '-00:00', + '+00:00', + '-07:00', + '-08:00', + '+05:45', + '+15:59', + '-15:59', + ]) { + for (const fraction of ['', '.000001', '.123400', '.999999']) { + const value = `${year}-${day}T12:34:56${fraction}${offset}` + if (normalizeTtlTimestamp(value) !== null) samples.push(value) + } + } + } + } + const normalized = samples.map((value) => normalizeTtlTimestamp(value)!) + const [result] = await control`SELECT count(*)::int AS mismatch FROM + unnest(${samples}::text[], ${normalized}::text[]) AS instants(input, normalized) + WHERE input::timestamptz != normalized::timestamptz` + expect(result.mismatch).toBe(0) + const guarded = await db.execute(sql`WITH samples AS MATERIALIZED ( + SELECT jsonb_array_elements_text(${JSON.stringify(samples)}::jsonb) AS value + ) SELECT count(*)::int AS mismatch FROM samples + WHERE ${validatedTimestampSql(sql`samples.value`, TTL_TIMESTAMP_VALIDATION)} + IS DISTINCT FROM samples.value::timestamptz`) + expect(guarded[0].mismatch).toBe(0) + measurements.postgresTimestampSamples = samples.length + }) + + it.skipIf(!process.env.TABLE_TTL_QA_STRESS_ROWS)( + 'drains a million-row backlog over bounded passes', + async () => { + const count = Number(process.env.TABLE_TTL_QA_STRESS_ROWS) + expect(count).toBeGreaterThanOrEqual(100000) + expect(count).toBeLessThanOrEqual(1000000) + const table = await createTable() + await seedRows(table, count) + await control`INSERT INTO user_table_rows (id, table_id, workspace_id, data) + VALUES (${generateId()}, ${table}, ${workspaceId}, ${control.json({ expires: future })}), + (${generateId()}, ${table}, ${workspaceId}, ${control.json({ expires: null })})` + const started = performance.now() + let deleted = 0 + let passes = 0 + let maxRss = process.memoryUsage().rss + while (deleted < count) { + const result = await runCleanupTableRowTtl() + expect(result.batches).toBeLessThanOrEqual(100) + expect(result.deleted).toBeGreaterThan(0) + deleted += result.deleted + passes++ + maxRss = Math.max(maxRss, process.memoryUsage().rss) + expect(passes).toBeLessThanOrEqual( + Math.ceil(count / (100 * getDeleteSnapshotBatchSize())) + 1 + ) + fireTrigger.mockClear() + } + expect(deleted).toBe(count) + expect(await rowCount(table)).toBe(2) + measurements.stress = { + rows: count, + passes, + deleted, + survivors: 2, + elapsedMs: Math.round(performance.now() - started), + maxRss, + } + }, + 300000 + ) +}) diff --git a/apps/sim/background/cleanup-table-row-ttl.test.ts b/apps/sim/background/cleanup-table-row-ttl.test.ts index b73e0c4748d..0e6948effbd 100644 --- a/apps/sim/background/cleanup-table-row-ttl.test.ts +++ b/apps/sim/background/cleanup-table-row-ttl.test.ts @@ -3,6 +3,7 @@ */ import type { SQL } from 'drizzle-orm' import { PgDialect } from 'drizzle-orm/pg-core' +import postgres from 'postgres' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.unmock('@sim/db/schema') @@ -16,6 +17,8 @@ const { mockTask, mockWithLockedTable, mockFireTableTrigger, + mockLoggerError, + mockLoggerInfo, } = vi.hoisted(() => ({ mockDeleteExecute: vi.fn(), mockListExecute: vi.fn(), @@ -24,11 +27,16 @@ const { mockTask: vi.fn((config: unknown) => config), mockWithLockedTable: vi.fn(), mockFireTableTrigger: vi.fn(), + mockLoggerError: vi.fn(), + mockLoggerInfo: vi.fn(), })) vi.mock('@sim/db', () => ({ dbFor: vi.fn(() => ({ execute: mockListExecute })), })) +vi.mock('@sim/logger', () => ({ + createLogger: () => ({ info: mockLoggerInfo, warn: vi.fn(), error: mockLoggerError }), +})) vi.mock('@trigger.dev/sdk', () => ({ task: mockTask })) vi.mock('@/lib/table/events', () => ({ signalTableRowsChanged: mockSignalTableRowsChanged })) @@ -85,6 +93,101 @@ describe('table row TTL cleanup', () => { ) }) + it.skipIf(!process.env.TABLE_TTL_TEST_DATABASE_URL)( + 'deletes only expired UTC cells in PostgreSQL with a non-UTC session', + async () => { + const client = postgres(process.env.TABLE_TTL_TEST_DATABASE_URL!, { max: 1 }) + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-09-07T12:00:00.500Z')) + try { + await client`SET TIME ZONE 'America/Los_Angeles'` + await client`CREATE TEMP TABLE user_table_definitions (id text, workspace_id text, schema jsonb, archived_at timestamp, delete_locked boolean)` + await client`CREATE TEMP TABLE user_table_rows (id text, table_id text, workspace_id text, data jsonb, created_at timestamp DEFAULT now())` + await client`INSERT INTO user_table_definitions VALUES (${table.id}, ${table.workspaceId}, ${client.json(table.schema)}, NULL, false)` + const values = { + expired: '2026-09-07T11:59:59Z', + equal: '2026-09-07T12:00:00Z', + future: '2026-09-07T12:00:01Z', + blank: null, + epoch: 1_700_000_000, + invalid: 'not-a-date', + invalid_day: '2026-02-30T12:00:00Z', + invalid_month: '2026-13-01T12:00:00Z', + invalid_year: '0000-01-01T00:00:00Z', + invalid_leap_day: '2025-02-29T12:00:00Z', + invalid_century_leap_day: '1900-02-29T12:00:00Z', + invalid_month_end: '2026-04-31T12:00:00Z', + invalid_hour: '2026-09-06T24:00:00Z', + invalid_minute: '2026-09-06T12:60:00Z', + invalid_second: '2026-09-06T12:00:60Z', + leap_day: '2024-02-29T12:00:00Z', + century_leap_day: '2000-02-29T12:00:00Z', + first_year: '0001-01-01T00:00:00Z', + last_year: '9999-12-31T23:59:59Z', + offset: '2026-09-06T12:00:00+00:00', + fraction: '2026-09-06T12:00:00.000Z', + negative_offset: '2026-09-07T04:59:59-07:00', + positive_offset: '2026-09-07T18:00:00+06:00', + future_offset: '2026-09-07T12:00:00-07:00', + equal_fraction: '2026-09-07T12:00:00.500000Z', + future_microsecond: '2026-09-07T12:00:00.500001Z', + minute_precision: '2026-09-07T12:00Z', + invalid_offset_day: '2026-02-30T12:00:00-07:00', + invalid_offset: '2026-09-07T12:00:00+16:00', + invalid_fraction: '2026-09-07T12:00:00.0000001Z', + rounding_future: '2026-09-07T12:00:00.5000001Z', + no_offset: '2020-01-01T00:00:00', + day_only: '2020-01-01', + relative_now: 'now', + relative_today: 'today', + relative_yesterday: 'yesterday', + epoch_alias: 'epoch', + past_infinity: '-infinity', + compact_offset: '2020-01-01T00:00:00+0000', + named_zone: '2020-01-01 00:00:00 America/Los_Angeles', + trailing_newline: '2020-01-01T00:00:00Z\n', + } + for (const [id, value] of Object.entries(values)) { + await client`INSERT INTO user_table_rows (id, table_id, workspace_id, data) VALUES (${id}, ${table.id}, ${table.workspaceId}, ${client.json({ 'col-ttl': value })})` + } + const execute = (statement: SQL) => { + const query = dialect.sqlToQuery(statement) + return client.unsafe(query.sql, query.params as (string | number)[]) + } + mockListExecute.mockImplementation(execute) + mockDeleteExecute.mockImplementation(execute) + expect(await runCleanupTableRowTtl()).toEqual({ + batches: 2, + deleted: 11, + limitReached: false, + }) + const remaining = await client<{ id: string }[]>`SELECT id FROM user_table_rows ORDER BY id` + expect(remaining.map(({ id }) => id)).toEqual( + Object.keys(values) + .filter( + (id) => + ![ + 'expired', + 'equal', + 'leap_day', + 'century_leap_day', + 'first_year', + 'offset', + 'fraction', + 'negative_offset', + 'positive_offset', + 'equal_fraction', + 'minute_precision', + ].includes(id) + ) + .sort() + ) + } finally { + nowSpy.mockRestore() + await client.end() + } + } + ) + it('deletes expired rows in locked, created-at keyset batches and signals the table', async () => { mockDeleteExecute .mockResolvedValueOnce([ @@ -123,9 +226,9 @@ describe('table row TTL cleanup', () => { ) }) - it('compares TTL values with whole Date.now epoch seconds', async () => { + it('compares TTL timestamps with the current UTC instant', async () => { const nowEpochMilliseconds = 1_700_000_000_999 - const nowEpochSeconds = 1_700_000_000 + const nowUtc = '2023-11-14T22:13:20.999Z' const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(nowEpochMilliseconds) mockDeleteExecute.mockResolvedValue([]) @@ -135,12 +238,8 @@ describe('table row TTL cleanup', () => { nowSpy.mockRestore() } - expect(dialect.sqlToQuery(mockListExecute.mock.calls[0][0] as SQL).params).toContain( - nowEpochSeconds - ) - expect(dialect.sqlToQuery(mockDeleteExecute.mock.calls[0][0] as SQL).params).toContain( - nowEpochSeconds - ) + expect(dialect.sqlToQuery(mockListExecute.mock.calls[0][0] as SQL).params).toContain(nowUtc) + expect(dialect.sqlToQuery(mockDeleteExecute.mock.calls[0][0] as SQL).params).toContain(nowUtc) }) it('checks the oldest expired rows first without using creation time as an expiry rule', async () => { @@ -153,7 +252,7 @@ describe('table row TTL cleanup', () => { .sql.replace(/\s+/g, ' ') .replace(/\$\d+/g, '?') .trim() - expect(query).toContain('AND (table_row.data->>?)::numeric <= ?') + expect(query).toContain('THEN (table_row.data->>?)::timestamptz END <= ?::timestamptz') expect(query).toContain('ORDER BY table_row.created_at, table_row.id') expect(query).toContain('octet_length(table_row.data::text) AS snapshot_bytes') expect(query).toContain('cumulative_snapshot_bytes <= ?') @@ -165,12 +264,23 @@ describe('table row TTL cleanup', () => { expect(query).not.toContain('table_row.created_by') }) - it('rejects a batch without a creation-time cursor', async () => { + it('skips a table whose batch has no creation-time cursor without signaling deletion', async () => { mockDeleteExecute.mockResolvedValue([{ id: 'row-1', data: { value: 1 } }]) - await expect(runCleanupTableRowTtl()).rejects.toThrow( - 'Table row TTL cleanup did not return a creation-time cursor' + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 1, + deleted: 0, + limitReached: false, + }) + expect(mockLoggerError).toHaveBeenCalledWith( + 'Table row TTL cleanup failed; skipping table for this run', + expect.objectContaining({ + tableId: table.id, + error: new Error('Table row TTL cleanup did not return a creation-time cursor'), + }) ) + expect(mockFireTableTrigger).not.toHaveBeenCalled() + expect(mockSignalTableRowsChanged).not.toHaveBeenCalled() }) it('does no work when already aborted', async () => { @@ -265,23 +375,123 @@ describe('table row TTL cleanup', () => { expect(mockSignalTableRowsChanged).toHaveBeenCalledWith(secondTable.id) }) - it('signals tables changed before a later table cleanup failure propagates', async () => { - const secondTable = { - ...table, - id: 'table-2', - } + it('skips a failed table, finishes healthy tables, and retries the failed table next run', async () => { + const secondTable = { ...table, id: 'table-2' } mockListExecute.mockResolvedValue([ { id: table.id, workspaceId: table.workspaceId }, { id: secondTable.id, workspaceId: secondTable.workspaceId }, ]) + mockDeleteExecute.mockResolvedValueOnce(returnedRows(1)).mockResolvedValue([]) mockWithLockedTable.mockImplementation(async (tableId, mutate) => { - if (tableId === secondTable.id) throw new Error('second table cleanup failed') - return mutate(table, { execute: vi.fn().mockResolvedValue(returnedRows(1)) }) + if (tableId === table.id) throw new Error('first table cleanup failed') + return mutate(secondTable, { execute: mockDeleteExecute }) }) - await expect(runCleanupTableRowTtl()).rejects.toThrow('second table cleanup failed') + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 3, + deleted: 1, + limitReached: false, + }) + expect(mockWithLockedTable.mock.calls.map(([id]) => id)).toEqual([ + table.id, + secondTable.id, + secondTable.id, + ]) expect(mockSignalTableRowsChanged).toHaveBeenCalledTimes(1) + expect(mockSignalTableRowsChanged).toHaveBeenCalledWith(secondTable.id) + expect(mockLoggerError).toHaveBeenCalledWith( + 'Table row TTL cleanup failed; skipping table for this run', + { + tableId: table.id, + workspaceId: table.workspaceId, + deleted: 0, + error: new Error('first table cleanup failed'), + } + ) + expect(mockLoggerInfo).toHaveBeenCalledWith('Table row TTL cleanup completed', { + batches: 3, + deleted: 1, + failedTables: 1, + limitReached: false, + }) + + mockListExecute.mockResolvedValue([{ id: table.id, workspaceId: table.workspaceId }]) + mockWithLockedTable.mockImplementation(async (_tableId, mutate) => + mutate(table, { execute: mockDeleteExecute }) + ) + mockDeleteExecute.mockClear().mockResolvedValueOnce(returnedRows(1)) + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 2, + deleted: 1, + limitReached: false, + }) + const retryQuery = dialect.sqlToQuery(mockDeleteExecute.mock.calls[0][0] as SQL) + expect(retryQuery.sql).not.toContain('(table_row.created_at, table_row.id) >') + expect(mockSignalTableRowsChanged).toHaveBeenCalledWith(table.id) + }) + + it('keeps and signals prior commits when a later batch fails while other tables finish', async () => { + const secondTable = { ...table, id: 'table-2' } + const firstExecute = vi + .fn() + .mockResolvedValueOnce(returnedRows(1)) + .mockRejectedValue(new Error('later batch failed')) + const secondExecute = vi.fn().mockResolvedValueOnce(returnedRows(1)).mockResolvedValue([]) + mockListExecute.mockResolvedValue([ + { id: table.id, workspaceId: table.workspaceId }, + { id: secondTable.id, workspaceId: secondTable.workspaceId }, + ]) + mockWithLockedTable.mockImplementation(async (tableId, mutate) => + tableId === table.id + ? mutate(table, { execute: firstExecute }) + : mutate(secondTable, { execute: secondExecute }) + ) + + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 4, + deleted: 2, + limitReached: false, + }) + expect(mockWithLockedTable.mock.calls.map(([id]) => id)).toEqual([ + table.id, + secondTable.id, + table.id, + secondTable.id, + ]) + expect(mockSignalTableRowsChanged).toHaveBeenCalledTimes(2) expect(mockSignalTableRowsChanged).toHaveBeenCalledWith(table.id) + expect(mockSignalTableRowsChanged).toHaveBeenCalledWith(secondTable.id) + expect(mockFireTableTrigger).toHaveBeenCalledTimes(2) + }) + + it('counts a failed attempt toward the run limit without repeatedly retrying that table', async () => { + const secondTable = { ...table, id: 'table-2' } + mockListExecute.mockResolvedValue([ + { id: table.id, workspaceId: table.workspaceId }, + { id: secondTable.id, workspaceId: secondTable.workspaceId }, + ]) + mockDeleteExecute.mockResolvedValue(returnedRows(500)) + mockWithLockedTable.mockImplementation(async (tableId, mutate) => { + if (tableId === table.id) throw new Error('persistent table failure') + return mutate(secondTable, { execute: mockDeleteExecute }) + }) + + await expect(runCleanupTableRowTtl()).resolves.toEqual({ + batches: 100, + deleted: 49_500, + limitReached: true, + }) + expect(mockWithLockedTable).toHaveBeenCalledTimes(100) + expect(mockWithLockedTable.mock.calls.filter(([id]) => id === table.id)).toHaveLength(1) + expect(mockDeleteExecute).toHaveBeenCalledTimes(99) + }) + + it('still rejects when table discovery fails before any table can be processed', async () => { + mockListExecute.mockRejectedValue(new Error('database unavailable')) + + await expect(runCleanupTableRowTtl()).rejects.toThrow('database unavailable') + expect(mockWithLockedTable).not.toHaveBeenCalled() + expect(mockSignalTableRowsChanged).not.toHaveBeenCalled() }) it('registers one serialized Trigger.dev task', () => { diff --git a/apps/sim/background/cleanup-table-row-ttl.ts b/apps/sim/background/cleanup-table-row-ttl.ts index 9b05e26e702..9a40bbafdaf 100644 --- a/apps/sim/background/cleanup-table-row-ttl.ts +++ b/apps/sim/background/cleanup-table-row-ttl.ts @@ -2,9 +2,10 @@ import { dbFor } from '@sim/db' import { userTableDefinitions, userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' -import { sql } from 'drizzle-orm' +import { type SQL, sql } from 'drizzle-orm' import { asOrchestrationError } from '@/lib/core/orchestration/types' import { getColumnId } from '@/lib/table/column-keys' +import { validatedTimestampSql } from '@/lib/table/column-types/timestamp-sql' import { getDeleteSnapshotBatchSize, TABLE_LIMITS } from '@/lib/table/constants' import { signalTableRowsChanged } from '@/lib/table/events' import { assertRowDelete, TableLockedError } from '@/lib/table/mutation-locks' @@ -13,6 +14,7 @@ import type { DeletedTableRow } from '@/lib/table/rows/ordering' import { withLockedTable } from '@/lib/table/service' import { fireTableTrigger } from '@/lib/table/trigger' import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability' +import { TTL_TIMESTAMP_VALIDATION } from '@/lib/table/ttl-values' import type { RowData, TableSchema } from '@/lib/table/types' const logger = createLogger('CleanupTableRowTtl') @@ -58,7 +60,12 @@ export interface TableRowTtlCleanupResult { limitReached: boolean } -async function listExpiredTtlTables(nowEpochSeconds: number): Promise { +/** Shares PostgreSQL's validated instant projection with Expiration comparisons. */ +function expiredTtlPredicate(cell: SQL, nowUtc: string): SQL { + return sql`${validatedTimestampSql(cell, TTL_TIMESTAMP_VALIDATION)} <= ${nowUtc}::timestamptz` +} + +async function listExpiredTtlTables(nowUtc: string): Promise { const rows = await cleanupDb.execute(sql` SELECT ${userTableDefinitions.id} AS id, @@ -75,21 +82,16 @@ async function listExpiredTtlTables(nowEpochSeconds: number): Promise>'type' = 'ttl' - AND jsonb_typeof( - table_row.data->COALESCE( - ttl_column.column_definition->>'id', - ttl_column.column_definition->>'name' - ) - ) = 'number' - AND ( - table_row.data->>COALESCE( - ttl_column.column_definition->>'id', - ttl_column.column_definition->>'name' - ) - )::numeric <= ${nowEpochSeconds} + AND ${expiredTtlPredicate( + sql`table_row.data->>COALESCE( + ttl_column.column_definition->>'id', + ttl_column.column_definition->>'name' + )`, + nowUtc + )} ) ORDER BY - md5(${userTableDefinitions.id} || ${nowEpochSeconds}::text), + md5(${userTableDefinitions.id} || ${nowUtc}::text), ${userTableDefinitions.id} LIMIT ${TTL_CLEANUP_MAX_BATCHES} `) @@ -144,7 +146,7 @@ async function deleteExpiredTableRowBatch( tableId: string, workspaceId: string, columnKey: string, - nowEpochSeconds: number, + nowUtc: string, batchSize: number, after?: TtlCleanupCursor ): Promise { @@ -164,8 +166,7 @@ async function deleteExpiredTableRowBatch( ? sql`AND (table_row.created_at, table_row.id) > (${after.createdAt}::timestamp, ${after.id})` : sql`` } - AND jsonb_typeof(table_row.data->${columnKey}) = 'number' - AND (table_row.data->>${columnKey})::numeric <= ${nowEpochSeconds} + AND ${expiredTtlPredicate(sql`table_row.data->>${columnKey}`, nowUtc)} ORDER BY table_row.created_at, table_row.id LIMIT ${batchSize} FOR UPDATE OF table_row SKIP LOCKED @@ -201,7 +202,7 @@ async function deleteExpiredTableRowBatch( async function deleteExpiredRowsForTable( ref: ExpiredTtlTableRef, - nowEpochSeconds: number, + nowUtc: string, batchSize: number, after?: TtlCleanupCursor ): Promise { @@ -226,7 +227,7 @@ async function deleteExpiredRowsForTable( table.id, table.workspaceId, getColumnId(ttlColumn), - nowEpochSeconds, + nowUtc, batchSize, after ) @@ -260,7 +261,7 @@ async function deleteExpiredRowsForTable( } } -/** Deletes rows whose table TTL cell is at or before the current Unix epoch second. */ +/** Deletes rows whose table TTL cell is at or before the current UTC instant. */ export async function runCleanupTableRowTtl( signal?: AbortSignal ): Promise { @@ -270,9 +271,9 @@ export async function runCleanupTableRowTtl( return { batches: 0, deleted: 0, limitReached: false } } - const nowEpochSeconds = Math.floor(Date.now() / 1000) + const nowUtc = new Date(Date.now()).toISOString() const batchSize = getDeleteSnapshotBatchSize() - const tableRefs = await listExpiredTtlTables(nowEpochSeconds) + const tableRefs = await listExpiredTtlTables(nowUtc) const tableStates: TtlTableCleanupState[] = tableRefs.map((ref) => ({ ref, deleted: 0, @@ -280,6 +281,7 @@ export async function runCleanupTableRowTtl( })) let deleted = 0 let batches = 0 + let failedTables = 0 try { while ( @@ -291,12 +293,21 @@ export async function runCleanupTableRowTtl( if (state.complete) continue if (batches === TTL_CLEANUP_MAX_BATCHES || signal?.aborted) break - const batch = await deleteExpiredRowsForTable( - state.ref, - nowEpochSeconds, - batchSize, - state.after - ) + let batch: DeletedTtlBatch + try { + batch = await deleteExpiredRowsForTable(state.ref, nowUtc, batchSize, state.after) + } catch (error) { + batches++ + failedTables++ + state.complete = true + logger.error('Table row TTL cleanup failed; skipping table for this run', { + tableId: state.ref.id, + workspaceId: state.ref.workspaceId, + deleted: state.deleted, + error, + }) + continue + } if (!batch.attempted) { state.complete = true continue @@ -318,7 +329,7 @@ export async function runCleanupTableRowTtl( const limitReached = batches === TTL_CLEANUP_MAX_BATCHES && (tableStates.some((state) => !state.complete) || tableRefs.length === TTL_CLEANUP_MAX_BATCHES) - logger.info('Table row TTL cleanup completed', { batches, deleted, limitReached }) + logger.info('Table row TTL cleanup completed', { batches, deleted, failedTables, limitReached }) return { batches, deleted, limitReached } } diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 22fb32d4a10..dbf390a7c80 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -4665,7 +4665,7 @@ export const QueryUserTable: ToolCatalogEntry = { filter: { type: 'object', description: - 'Predicate filter object for query_rows. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are absolute whole Unix epoch seconds, never milliseconds. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}.', + 'Predicate filter object for query_rows. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}.', }, limit: { type: 'number', @@ -6018,7 +6018,7 @@ export const TableColumns: ToolCatalogEntry = { column: { type: 'object', description: - 'Column definition for add_column: { name, type, unique?, position? }; type may be string, number, boolean, date, json, select, or ttl. Select (enum) columns also take { options: [names], multiple?: true } — options is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Column definition for add_column: { name, type, unique?, position? }; type may be string, number, boolean, date, json, select, or ttl. Select (enum) columns also take { options: [names], multiple?: true } — options is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, columnName: { type: 'string', @@ -6040,7 +6040,7 @@ export const TableColumns: ToolCatalogEntry = { newType: { type: 'string', description: - 'New column type for update_column: string, number, boolean, date, json, select, ttl. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are absolute whole Unix epoch seconds, not milliseconds.', + 'New column type for update_column: string, number, boolean, date, json, select, ttl. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, options: { type: 'array', @@ -6219,7 +6219,7 @@ export const TableManage: ToolCatalogEntry = { schema: { type: 'object', description: - 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; types are string, number, boolean, date, json, select, and ttl. A select (enum) column also requires options (display names) and takes multiple?. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; types are string, number, boolean, date, json, select, and ttl. A select (enum) column also requires options (display names) and takes multiple?. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, tableId: { type: 'string', @@ -6265,12 +6265,12 @@ export const TableRows: ToolCatalogEntry = { data: { type: 'object', description: - 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME. TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds. On insert_row, a missing or null TTL means no expiration. On update_row and update_rows_by_filter, omit the TTL to preserve its current value or set it to null to clear the expiration.', + 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME. TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. On insert_row, a missing or null TTL means no expiration. On update_row and update_rows_by_filter, omit the TTL to preserve its current value or set it to null to clear the expiration.', }, filter: { type: 'object', description: - 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES. TTL filter values are absolute whole Unix epoch seconds.', + 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, limit: { type: 'number', @@ -6296,14 +6296,14 @@ export const TableRows: ToolCatalogEntry = { rows: { type: 'array', description: - 'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.', + 'Array of row data objects (required for batch_insert_rows). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; a missing or null TTL means no expiration.', items: { type: 'object' }, }, tableId: { type: 'string', description: 'Table ID (required for every operation)' }, updates: { type: 'array', description: - "Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.", + "Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; omit a row's TTL key to preserve it or set it to null to clear the expiration.", items: { type: 'object', properties: { data: { type: 'object' }, rowId: { type: 'string' } }, @@ -6313,7 +6313,7 @@ export const TableRows: ToolCatalogEntry = { values: { type: 'object', description: - "Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName). For a TTL column, values are absolute whole Unix epoch seconds, never JavaScript milliseconds; set a row's value to null to clear its expiration, and omit the row from the map to leave it unchanged.", + "Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName). For a TTL column, values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; set a row's value to null to clear its expiration, and omit the row from the map to leave it unchanged.", }, }, required: ['tableId'], @@ -6639,7 +6639,7 @@ export const UserTable: ToolCatalogEntry = { column: { type: 'object', description: - 'Column definition for add_column: { name, type, unique?, position? }. Type may be string, number, boolean, date, json, select, or ttl. For a select (enum) column also pass { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Column definition for add_column: { name, type, unique?, position? }. Type may be string, number, boolean, date, json, select, or ttl. For a select (enum) column also pass { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, columnName: { type: 'string', @@ -6660,7 +6660,7 @@ export const UserTable: ToolCatalogEntry = { data: { type: 'object', description: - 'Row data as key-value pairs (required for insert_row, update_row). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds. On insert_row, a missing or null TTL means no expiration. On update_row, omit the TTL to preserve its current value or set it to null to clear the expiration.', + 'Row data as key-value pairs (required for insert_row, update_row). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. On insert_row, a missing or null TTL means no expiration. On update_row, omit the TTL to preserve its current value or set it to null to clear the expiration.', }, dependencies: { type: 'object', @@ -6689,7 +6689,7 @@ export const UserTable: ToolCatalogEntry = { filter: { type: 'object', description: - 'Predicate filter object for query_rows, update_rows_by_filter, delete_rows_by_filter. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are absolute whole Unix epoch seconds, never milliseconds. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"all":[{"field":"wins","op":"gte","value":18},{"field":"status","op":"eq","value":"pending"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}; {"all":[{"field":"slack_user_id","op":"in","value":["U1","U2"]}]}.', + 'Predicate filter object for query_rows, update_rows_by_filter, delete_rows_by_filter. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"all":[{"field":"wins","op":"gte","value":18},{"field":"status","op":"eq","value":"pending"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}; {"all":[{"field":"slack_user_id","op":"in","value":["U1","U2"]}]}.', }, groupId: { type: 'string', @@ -6778,7 +6778,7 @@ export const UserTable: ToolCatalogEntry = { newType: { type: 'string', description: - 'New column type (optional for update_column). Types: string, number, boolean, date, json, select, ttl. Converting a column to select also requires options; the conversion fails if any existing cell value doesn\'t match one of them. Converting to a multiple: true select also accepts a comma-separated cell ("Open, Urgent"), which is the form a multi column converts to text as — so multiselect → text → multiselect round-trips. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are absolute whole Unix epoch seconds, not milliseconds.', + 'New column type (optional for update_column). Types: string, number, boolean, date, json, select, ttl. Converting a column to select also requires options; the conversion fails if any existing cell value doesn\'t match one of them. Converting to a multiple: true select also accepts a comma-separated cell ("Open, Urgent"), which is the form a multi column converts to text as — so multiselect → text → multiselect round-trips. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, options: { type: 'array', @@ -6865,7 +6865,7 @@ export const UserTable: ToolCatalogEntry = { rows: { type: 'array', description: - 'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.', + 'Array of row data objects (required for batch_insert_rows). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; a missing or null TTL means no expiration.', items: { type: 'object' }, }, runMode: { @@ -6877,7 +6877,7 @@ export const UserTable: ToolCatalogEntry = { schema: { type: 'object', description: - 'Table schema with columns array (required for \'create\'). Each column: { name, type, unique? }. Types are string, number, boolean, date, json, select, and ttl. A select (enum) column also takes { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Table schema with columns array (required for \'create\'). Each column: { name, type, unique? }. Types are string, number, boolean, date, json, select, and ttl. A select (enum) column also takes { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, scope: { type: 'string', @@ -6902,7 +6902,7 @@ export const UserTable: ToolCatalogEntry = { updates: { type: 'array', description: - "Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.", + "Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; omit a row's TTL key to preserve it or set it to null to clear the expiration.", items: { type: 'object', properties: { data: { type: 'object' }, rowId: { type: 'string' } }, @@ -6912,7 +6912,7 @@ export const UserTable: ToolCatalogEntry = { values: { type: 'object', description: - 'Map of rowId to value for single-column batch update: { "rowId1": val1, "rowId2": val2 } (for batch_update_rows with columnName). For a TTL column, values are absolute whole Unix epoch seconds, never JavaScript milliseconds; set a row\'s value to null to clear its expiration, and omit the row from the map to leave it unchanged.', + 'Map of rowId to value for single-column batch update: { "rowId1": val1, "rowId2": val2 } (for batch_update_rows with columnName). For a TTL column, values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; set a row\'s value to null to clear its expiration, and omit the row from the map to leave it unchanged.', }, workflowId: { type: 'string', diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 72e603a3613..cb977c36ed5 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -4592,7 +4592,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { filter: { type: 'object', description: - 'Predicate filter object for query_rows. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are absolute whole Unix epoch seconds, never milliseconds. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}.', + 'Predicate filter object for query_rows. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}.', }, limit: { type: 'number', @@ -5938,7 +5938,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { column: { type: 'object', description: - 'Column definition for add_column: { name, type, unique?, position? }; type may be string, number, boolean, date, json, select, or ttl. Select (enum) columns also take { options: [names], multiple?: true } — options is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Column definition for add_column: { name, type, unique?, position? }; type may be string, number, boolean, date, json, select, or ttl. Select (enum) columns also take { options: [names], multiple?: true } — options is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, columnName: { type: 'string', @@ -5965,7 +5965,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { newType: { type: 'string', description: - 'New column type for update_column: string, number, boolean, date, json, select, ttl. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are absolute whole Unix epoch seconds, not milliseconds.', + 'New column type for update_column: string, number, boolean, date, json, select, ttl. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, options: { type: 'array', @@ -6171,7 +6171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { schema: { type: 'object', description: - 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; types are string, number, boolean, date, json, select, and ttl. A select (enum) column also requires options (display names) and takes multiple?. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; types are string, number, boolean, date, json, select, and ttl. A select (enum) column also requires options (display names) and takes multiple?. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, tableId: { type: 'string', @@ -6221,12 +6221,12 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { data: { type: 'object', description: - 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME. TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds. On insert_row, a missing or null TTL means no expiration. On update_row and update_rows_by_filter, omit the TTL to preserve its current value or set it to null to clear the expiration.', + 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME. TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. On insert_row, a missing or null TTL means no expiration. On update_row and update_rows_by_filter, omit the TTL to preserve its current value or set it to null to clear the expiration.', }, filter: { type: 'object', description: - 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES. TTL filter values are absolute whole Unix epoch seconds.', + 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, limit: { type: 'number', @@ -6257,7 +6257,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { rows: { type: 'array', description: - 'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.', + 'Array of row data objects (required for batch_insert_rows). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; a missing or null TTL means no expiration.', items: { type: 'object', }, @@ -6269,7 +6269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { updates: { type: 'array', description: - "Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.", + "Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a). TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; omit a row's TTL key to preserve it or set it to null to clear the expiration.", items: { type: 'object', properties: { @@ -6286,7 +6286,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { values: { type: 'object', description: - "Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName). For a TTL column, values are absolute whole Unix epoch seconds, never JavaScript milliseconds; set a row's value to null to clear its expiration, and omit the row from the map to leave it unchanged.", + "Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName). For a TTL column, values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; set a row's value to null to clear its expiration, and omit the row from the map to leave it unchanged.", }, }, required: ['tableId'], @@ -6629,7 +6629,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { column: { type: 'object', description: - 'Column definition for add_column: { name, type, unique?, position? }. Type may be string, number, boolean, date, json, select, or ttl. For a select (enum) column also pass { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Column definition for add_column: { name, type, unique?, position? }. Type may be string, number, boolean, date, json, select, or ttl. For a select (enum) column also pass { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, columnName: { type: 'string', @@ -6652,7 +6652,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { data: { type: 'object', description: - 'Row data as key-value pairs (required for insert_row, update_row). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds. On insert_row, a missing or null TTL means no expiration. On update_row, omit the TTL to preserve its current value or set it to null to clear the expiration.', + 'Row data as key-value pairs (required for insert_row, update_row). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. On insert_row, a missing or null TTL means no expiration. On update_row, omit the TTL to preserve its current value or set it to null to clear the expiration.', }, dependencies: { type: 'object', @@ -6686,7 +6686,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { filter: { type: 'object', description: - 'Predicate filter object for query_rows, update_rows_by_filter, delete_rows_by_filter. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are absolute whole Unix epoch seconds, never milliseconds. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"all":[{"field":"wins","op":"gte","value":18},{"field":"status","op":"eq","value":"pending"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}; {"all":[{"field":"slack_user_id","op":"in","value":["U1","U2"]}]}.', + 'Predicate filter object for query_rows, update_rows_by_filter, delete_rows_by_filter. A predicate is a tree: {"all":[...]} (AND) or {"any":[...]} (OR); members are leaves {field, op, value} or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (use * as the wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array value. TTL filter values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00. Examples: {"all":[{"field":"status","op":"eq","value":"active"}]}; {"all":[{"field":"wins","op":"gte","value":18},{"field":"status","op":"eq","value":"pending"}]}; {"any":[{"field":"status","op":"eq","value":"active"},{"field":"status","op":"eq","value":"pending"}]}; {"all":[{"field":"name","op":"ilike","value":"*jo*"}]}; {"all":[{"field":"slack_user_id","op":"in","value":["U1","U2"]}]}.', }, groupId: { type: 'string', @@ -6783,7 +6783,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { newType: { type: 'string', description: - 'New column type (optional for update_column). Types: string, number, boolean, date, json, select, ttl. Converting a column to select also requires options; the conversion fails if any existing cell value doesn\'t match one of them. Converting to a multiple: true select also accepts a comma-separated cell ("Open, Urgent"), which is the form a multi column converts to text as — so multiselect → text → multiselect round-trips. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are absolute whole Unix epoch seconds, not milliseconds.', + 'New column type (optional for update_column). Types: string, number, boolean, date, json, select, ttl. Converting a column to select also requires options; the conversion fails if any existing cell value doesn\'t match one of them. Converting to a multiple: true select also accepts a comma-separated cell ("Open, Urgent"), which is the form a multi column converts to text as — so multiselect → text → multiselect round-trips. Converting to ttl enables row expiration and fails if the table already has another ttl column; TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, options: { type: 'array', @@ -6885,7 +6885,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { rows: { type: 'array', description: - 'Array of row data objects (required for batch_insert_rows). TTL cells take absolute whole Unix epoch seconds, never JavaScript milliseconds; a missing or null TTL means no expiration.', + 'Array of row data objects (required for batch_insert_rows). TTL cells take ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; a missing or null TTL means no expiration.', items: { type: 'object', }, @@ -6899,7 +6899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { schema: { type: 'object', description: - 'Table schema with columns array (required for \'create\'). Each column: { name, type, unique? }. Types are string, number, boolean, date, json, select, and ttl. A select (enum) column also takes { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, with cell values stored as absolute whole Unix epoch seconds rather than milliseconds.', + 'Table schema with columns array (required for \'create\'). Each column: { name, type, unique? }. Types are string, number, boolean, date, json, select, and ttl. A select (enum) column also takes { options: ["Open", "Closed"], multiple?: true } — options is a list of display names and is required for select. A table may have at most one ttl column; adding it enables row expiration, accepting ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00.', }, scope: { type: 'string', @@ -6926,7 +6926,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { updates: { type: 'array', description: - "Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are absolute whole Unix epoch seconds, never JavaScript milliseconds; omit a row's TTL key to preserve it or set it to null to clear the expiration.", + "Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows). TTL values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; omit a row's TTL key to preserve it or set it to null to clear the expiration.", items: { type: 'object', properties: { @@ -6943,7 +6943,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { values: { type: 'object', description: - 'Map of rowId to value for single-column batch update: { "rowId1": val1, "rowId2": val2 } (for batch_update_rows with columnName). For a TTL column, values are absolute whole Unix epoch seconds, never JavaScript milliseconds; set a row\'s value to null to clear its expiration, and omit the row from the map to leave it unchanged.', + 'Map of rowId to value for single-column batch update: { "rowId1": val1, "rowId2": val2 } (for batch_update_rows with columnName). For a TTL column, values are ISO timestamp strings with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00); up to 6 fractional second digits are accepted; supplied numeric offsets are preserved, and Z is stored as -00:00; set a row\'s value to null to clear its expiration, and omit the row from the map to leave it unchanged.', }, workflowId: { type: 'string', diff --git a/apps/sim/lib/core/utils/timezone.test.ts b/apps/sim/lib/core/utils/timezone.test.ts index 64ce3f793d4..bd5f7db3cac 100644 --- a/apps/sim/lib/core/utils/timezone.test.ts +++ b/apps/sim/lib/core/utils/timezone.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { - formatInstantInTimeZone, getSupportedTimezones, getTimezoneOptions, getWallClockParts, @@ -11,41 +10,7 @@ import { zonedWallClockWithOffset, } from '@/lib/core/utils/timezone' -describe('formatInstantInTimeZone', () => { - it.each([ - ['UTC', '0050-01-15T12:00:00Z', '0050-01-15T12:00:00Z'], - ['UTC', '2026-06-15T00:15:30Z', '2026-06-15T00:15:30Z'], - ['America/Los_Angeles', '2026-06-15T00:15:30Z', '2026-06-14T17:15:30-07:00'], - ['Asia/Tokyo', '2026-06-15T00:15:30Z', '2026-06-15T09:15:30+09:00'], - ['Asia/Kathmandu', '2026-06-15T00:15:30Z', '2026-06-15T06:00:30+05:45'], - ['Australia/Lord_Howe', '2026-06-15T00:15:30Z', '2026-06-15T10:45:30+10:30'], - ])('formats an instant in %s with its exact offset', (timeZone, iso, expected) => { - expect(formatInstantInTimeZone(new Date(iso), timeZone)).toBe(expected) - }) - - it('distinguishes both copies of an autumn daylight-saving hour', () => { - expect(formatInstantInTimeZone(new Date('2026-11-01T05:30:00Z'), 'America/New_York')).toBe( - '2026-11-01T01:30:00-04:00' - ) - expect(formatInstantInTimeZone(new Date('2026-11-01T06:30:00Z'), 'America/New_York')).toBe( - '2026-11-01T01:30:00-05:00' - ) - }) - - it('round-trips the same instant after changing display timezones', () => { - const instant = new Date('2026-11-01T06:30:00Z') - for (const timeZone of [ - 'UTC', - 'America/Los_Angeles', - 'America/New_York', - 'Asia/Kathmandu', - 'Australia/Lord_Howe', - ]) { - const editable = formatInstantInTimeZone(instant, timeZone) - expect(new Date(editable).getTime()).toBe(instant.getTime()) - } - }) - +describe('zonedWallClock', () => { it('preserves a four-digit low year in naive wall-clock output', () => { expect(zonedWallClock(new Date('0050-01-15T12:00:00Z'), 'UTC')).toBe('0050-01-15T12:00') }) @@ -138,28 +103,20 @@ describe('zonedWallClockToUtc', () => { }) it.each([ - [ - 'Europe/Berlin', - '2026-03-29T02:30', - '2026-03-29T01:30:00.000Z', - '2026-03-29T03:30:00+02:00', - '2026-03-29T02:30+01:00', - ], + ['Europe/Berlin', '2026-03-29T02:30', '2026-03-29T01:30:00.000Z', '2026-03-29T02:30+01:00'], [ 'Australia/Lord_Howe', '2026-10-04T02:15', '2026-10-03T15:45:00.000Z', - '2026-10-04T02:45:00+11:00', '2026-10-04T02:15+10:30', ], ])( 'resolves an east-of-UTC spring-forward gap in %s to the first compatible wall-clock', - (timeZone, wallClock, expectedInstant, expectedRenderedWallClock, expectedStampedWallClock) => { + (timeZone, wallClock, expectedInstant, expectedStampedWallClock) => { const instant = zonedWallClockToUtc(wallClock, timeZone) const stampedWallClock = zonedWallClockWithOffset(wallClock, timeZone) expect(instant.toISOString()).toBe(expectedInstant) - expect(formatInstantInTimeZone(instant, timeZone)).toBe(expectedRenderedWallClock) expect(stampedWallClock).toBe(expectedStampedWallClock) expect(new Date(stampedWallClock).toISOString()).toBe(expectedInstant) } @@ -211,22 +168,6 @@ describe('zonedWallClockToUtc', () => { '2026-06-15T13:00:30.000Z' ) }) - - it('can serialize historical sub-minute offsets toward a later instant', () => { - const wallClock = '1970-01-01T00:00:00' - const timezone = 'Africa/Monrovia' - const exactInstant = zonedWallClockToUtc(wallClock, timezone) - const options = { offsetMinuteRounding: 'floor' as const } - - expect(exactInstant.toISOString()).toBe('1970-01-01T00:44:30.000Z') - expect(zonedWallClockWithOffset(wallClock, timezone, options)).toBe('1970-01-01T00:00:00-00:45') - expect(formatInstantInTimeZone(exactInstant, timezone, options)).toBe( - '1970-01-01T00:00:00-00:45' - ) - expect( - Date.parse(zonedWallClockWithOffset(wallClock, timezone, options)) - ).toBeGreaterThanOrEqual(exactInstant.getTime()) - }) }) describe('wallClockNow', () => { diff --git a/apps/sim/lib/core/utils/timezone.ts b/apps/sim/lib/core/utils/timezone.ts index e4d2ffea799..fe4e19c104e 100644 --- a/apps/sim/lib/core/utils/timezone.ts +++ b/apps/sim/lib/core/utils/timezone.ts @@ -205,19 +205,6 @@ export function getWallClockParts(instant: Date, timeZone?: string): WallClockPa } } -/** Formats an instant as an RFC 3339 wall time in an IANA timezone. */ -export function formatInstantInTimeZone( - instant: Date, - timeZone: string, - options?: ZonedWallClockOptions -): string { - const wall = getWallClockParts(instant, timeZone) - const wholeSecondInstant = new Date(Math.floor(instant.getTime() / 1000) * 1000) - const exactOffsetMinutes = offsetMsFromWallClock(wholeSecondInstant, wall) / 60_000 - const offsetMinutes = roundOffsetMinutes(exactOffsetMinutes, options) - return `${formatIsoYear(wall.year)}-${pad(wall.month)}-${pad(wall.day)}T${pad(wall.hour)}:${pad(wall.minute)}:${pad(wall.second)}${formatUtcOffsetSuffix(offsetMinutes)}` -} - /** * An instant's wall-clock time in `timeZone` as a naive `yyyy-MM-ddTHH:mm` * string. Lets callers reason about a user's local date/time without UTC — e.g. @@ -261,14 +248,6 @@ interface ZonedWallClockResolution { export interface ZonedWallClockOptions { /** Which real instant to use when the wall clock occurs twice during a DST fall-back. */ ambiguousTime?: 'earlier' | 'later' - /** How to serialize rare historical offsets containing seconds into RFC 3339 minutes. */ - offsetMinuteRounding?: 'nearest' | 'floor' -} - -function roundOffsetMinutes(exactOffsetMinutes: number, options?: ZonedWallClockOptions): number { - return options?.offsetMinuteRounding === 'floor' - ? Math.floor(exactOffsetMinutes) - : Math.round(exactOffsetMinutes) } function resolveZonedWallClock( @@ -333,6 +312,6 @@ export function zonedWallClockWithOffset( options?: ZonedWallClockOptions ): string { const resolution = resolveZonedWallClock(wallClock, timeZone, options) - const offsetMinutes = roundOffsetMinutes(resolution.offsetMinutes, options) + const offsetMinutes = Math.round(resolution.offsetMinutes) return `${wallClock}${formatUtcOffsetSuffix(offsetMinutes)}` } diff --git a/apps/sim/lib/table/__tests__/column-type-registry.test.ts b/apps/sim/lib/table/__tests__/column-type-registry.test.ts index a63886a4a5c..63c2466d702 100644 --- a/apps/sim/lib/table/__tests__/column-type-registry.test.ts +++ b/apps/sim/lib/table/__tests__/column-type-registry.test.ts @@ -10,7 +10,6 @@ * here. */ import { describe, expect, it } from 'vitest' -import { zonedWallClockToUtc } from '@/lib/core/utils/timezone' import type { ColumnType } from '@/lib/table/column-types' import { ALL_COLUMN_TYPES, @@ -112,7 +111,7 @@ describe('conversion write-back', () => { for (const definition of ALL_COLUMN_TYPES) { if (definition.jsonbCast !== 'timestamptz') continue expect(definition.coerce(1700000000000, { name: 'c', type: definition.id }).ok).toBe(false) - const coerced = definition.coerce('2023-11-14T22:13:20.000Z', { + const coerced = definition.coerce('2023-11-14T22:13:20Z', { name: 'c', type: definition.id, }) @@ -123,140 +122,14 @@ describe('conversion write-back', () => { }) describe('ttl columns', () => { - const column = { name: 'expires_at', type: 'ttl' } as ColumnDefinition - - it('stores integer epoch seconds while accepting date-shaped input', () => { - expect(COLUMN_TYPE_REGISTRY.ttl.coerce('2023-11-14T22:13:20Z', column)).toEqual({ - ok: true, - value: 1_700_000_000, - }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(1_700_000_000, column)).toEqual({ - ok: true, - value: 1_700_000_000, - }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce('1700000000', column)).toEqual({ - ok: true, - value: 1_700_000_000, - }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce('2023-11-14T22:13:20.123Z', column)).toEqual({ - ok: true, - value: 1_700_000_001, - }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce('not-a-date', column)).toEqual({ ok: false }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(1_700_000_000.5, column)).toEqual({ ok: false }) - }) - - it.each(['2023-02-29', '2023-02-29T12:00:00', '2023-02-29T12:00:00-05:00'])( - 'rejects a nonexistent ISO calendar input: %s', - (value) => { - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(value, column, { timezone: 'UTC' })).toEqual({ - ok: false, - }) - } - ) - - it.each([ - ['2024-02-29', '2024-02-29T00:00:00Z'], - ['2024-02-29T12:00:00', '2024-02-29T12:00:00Z'], - ['2024-02-29T12:00:00-05:00', '2024-02-29T17:00:00Z'], - ])('accepts a valid leap-day ISO calendar input: %s', (value, expectedInstant) => { - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(value, column, { timezone: 'UTC' })).toEqual({ - ok: true, - value: Math.floor(Date.parse(expectedInstant) / 1000), + it('declares offset-preserving editing, string workflow values, timestamp comparisons, and one column per table', () => { + expect(COLUMN_TYPE_REGISTRY.ttl).toMatchObject({ + jsonbCast: 'timestamptz', + workflowInputType: 'string', + editor: 'offset-date', + maxPerTable: 1, }) }) - - it('renders and edits epoch seconds as a date', () => { - expect(COLUMN_TYPE_REGISTRY.ttl.formatForDisplay(1_700_000_000, column)).toBe( - '11/14/2023 10:13:20 PM' - ) - expect(COLUMN_TYPE_REGISTRY.ttl.formatForInput(1_700_000_000, column)).toBe( - '2023-11-14T22:13:20Z' - ) - expect( - COLUMN_TYPE_REGISTRY.ttl.formatForInput(1_700_000_000, column, { - timezone: 'America/New_York', - }) - ).toBe('2023-11-14T17:13:20-05:00') - }) - - it('preserves the exact instant across both sides of a daylight-saving fold', () => { - expect( - COLUMN_TYPE_REGISTRY.ttl.formatForInput(1_699_162_200, column, { - timezone: 'America/New_York', - }) - ).toBe('2023-11-05T01:30:00-04:00') - expect( - COLUMN_TYPE_REGISTRY.ttl.formatForInput(1_699_165_800, column, { - timezone: 'America/New_York', - }) - ).toBe('2023-11-05T01:30:00-05:00') - }) - - it('matches the shared wall-clock resolver in every effective timezone', () => { - const wallClock = '2026-06-15T09:00:30' - for (const timezone of [ - 'UTC', - 'America/Los_Angeles', - 'America/New_York', - 'Asia/Kathmandu', - 'Australia/Lord_Howe', - ]) { - const expected = Math.floor(zonedWallClockToUtc(wallClock, timezone).getTime() / 1000) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(wallClock, column, { timezone })).toEqual({ - ok: true, - value: expected, - }) - } - }) - - it.each([ - ['Europe/Berlin', '2026-03-29T02:30'], - ['Australia/Lord_Howe', '2026-10-04T02:15'], - ])('coerces a %s spring-forward gap wall clock to the compatible epoch', (timezone, input) => { - const expected = Math.floor(zonedWallClockToUtc(input, timezone).getTime() / 1000) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(input, column, { timezone })).toEqual({ - ok: true, - value: expected, - }) - }) - - it('coerces a localized month-name gap input in the explicit workspace timezone', () => { - const timezone = 'America/New_York' - const expected = Math.floor(zonedWallClockToUtc('2026-03-08T02:30', timezone).getTime() / 1000) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce('March 8, 2026 2:30 AM', column, { timezone })).toEqual({ - ok: true, - value: expected, - }) - }) - - it('rejects an impossible ISO expiration date', () => { - expect( - COLUMN_TYPE_REGISTRY.ttl.coerce('2026-02-30T12:00:00', column, { timezone: 'UTC' }) - ).toEqual({ ok: false }) - }) - - it('round-trips epoch seconds after the editor timezone changes', () => { - for (const seconds of [1_700_000_000, 1_699_162_200, 1_699_165_800]) { - for (const timezone of [ - 'UTC', - 'America/Los_Angeles', - 'America/New_York', - 'Asia/Kathmandu', - 'Australia/Lord_Howe', - ]) { - const editable = COLUMN_TYPE_REGISTRY.ttl.formatForInput(seconds, column, { timezone }) - expect(COLUMN_TYPE_REGISTRY.ttl.coerce(editable, column, { timezone })).toEqual({ - ok: true, - value: seconds, - }) - } - } - }) - - it('limits a table to one ttl column', () => { - expect(COLUMN_TYPE_REGISTRY.ttl.maxPerTable).toBe(1) - }) }) describe('intentional divergences from the pre-registry behavior', () => { diff --git a/apps/sim/lib/table/__tests__/sql.test.ts b/apps/sim/lib/table/__tests__/sql.test.ts index 8623b06f7cf..e02664c11fd 100644 --- a/apps/sim/lib/table/__tests__/sql.test.ts +++ b/apps/sim/lib/table/__tests__/sql.test.ts @@ -1327,3 +1327,59 @@ describe('error messages name the caller-facing column, not the storage id', () expect(() => buildPredicateClause(p, TABLE, [num])).not.toThrow() }) }) + +describe('Expiration instant comparison SQL', () => { + const column: ColumnDefinition = { name: 'expires_at', type: 'ttl' } + const instant = '2026-09-07T14:30:00Z' + + it('casts expiration ranges and sorting as timestamps', () => { + const range = renderSql( + buildPredicateClause({ field: 'expires_at', op: 'lte', value: instant }, 'user_table_rows', [ + column, + ]) + ) + expect(range).toContain("(user_table_rows.data->>'expires_at')::timestamptz") + expect(range).toContain(instant) + expect( + renderSql(buildSortClause({ expires_at: 'asc' }, 'user_table_rows', [column])) + ).toContain('::timestamptz ASC') + }) + + it('compares equality and membership using timestamp casts', () => { + expect( + renderSql(fieldPredicate('user_table_rows', 'expires_at', 'eq', instant, column)) + ).toContain('::timestamptz') + expect( + renderSql(fieldPredicate('user_table_rows', 'expires_at', 'in', [instant], column)) + ).toContain('::timestamptz') + }) + + it.each(['eq', 'ne', 'in', 'nin'] as const)( + 'matches equivalent offset and fractional representations for %s', + (op) => { + const input = '2026-09-07T07:30:00.000-07:00' + const value = op === 'in' || op === 'nin' ? [input] : input + const query = renderSql(fieldPredicate('user_table_rows', 'expires_at', op, value, column)) + expect(query).toContain('::timestamptz') + expect(query).toContain('2026-09-07T07:30:00-07:00') + expect(query).not.toContain('@>') + } + ) + + it.each(['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin'] as const)( + 'refuses ambiguous or invalid %s operands before querying', + (op) => { + for (const invalid of [ + 1_700_000_000, + '2026-09-07', + '2026-09-07T14:30:00', + '2026-02-30T14:30:00Z', + ]) { + const value = op === 'in' || op === 'nin' ? [invalid] : invalid + expect(() => fieldPredicate('user_table_rows', 'expires_at', op, value, column)).toThrow( + 'Z or an explicit UTC offset' + ) + } + } + ) +}) diff --git a/apps/sim/lib/table/__tests__/validation.test.ts b/apps/sim/lib/table/__tests__/validation.test.ts index 04b70ce4af9..76c935d101c 100644 --- a/apps/sim/lib/table/__tests__/validation.test.ts +++ b/apps/sim/lib/table/__tests__/validation.test.ts @@ -655,6 +655,32 @@ describe('Validation', () => { expect(result.valid).toBe(true) }) + it('compares expiration uniqueness by instant while retaining microseconds', () => { + const expirationSchema: TableSchema = { + columns: [{ name: 'expires', type: 'ttl', unique: true }], + } + const rows = [{ id: 'existing', data: { expires: '2026-09-07T07:30:00.000001-07:00' } }] + for (const value of [ + '2026-09-07T14:30:00.000001Z', + '2026-09-07T20:15:00.000001+05:45', + '2026-09-07T14:30:00.000001-00:00', + ]) { + expect(validateUniqueConstraints({ expires: value }, expirationSchema, rows).valid).toBe( + false + ) + expect( + validateUniqueConstraints({ expires: value }, expirationSchema, rows, 'existing').valid + ).toBe(true) + } + expect( + validateUniqueConstraints( + { expires: '2026-09-07T14:30:00.000002-00:00' }, + expirationSchema, + rows + ).valid + ).toBe(true) + }) + it('should report multiple violations', () => { const data = { id: 'abc123', email: 'john@example.com', name: 'New User' } const result = validateUniqueConstraints(data, schema, existingRows) diff --git a/apps/sim/lib/table/column-types/comparison-sql.ts b/apps/sim/lib/table/column-types/comparison-sql.ts new file mode 100644 index 00000000000..03a9d20cea3 --- /dev/null +++ b/apps/sim/lib/table/column-types/comparison-sql.ts @@ -0,0 +1,14 @@ +import { type SQL, sql } from 'drizzle-orm' +import { columnTypeOf } from '@/lib/table/column-types/registry' +import { validatedTimestampSql } from '@/lib/table/column-types/timestamp-sql' +import type { ColumnDefinition } from '@/lib/table/types' + +/** Casts stored text for equality, treating malformed explicit timestamps as absent instants. */ +export function columnTextForEquality(cell: SQL, column: ColumnDefinition): SQL { + const definition = columnTypeOf(column) + if (!definition.valueForEquality || !definition.jsonbCast) return cell + if (definition.timestampValidation) { + return validatedTimestampSql(cell, definition.timestampValidation) + } + return sql`(${cell})::${sql.raw(definition.jsonbCast)}` +} diff --git a/apps/sim/lib/table/column-types/extension-points.test.ts b/apps/sim/lib/table/column-types/extension-points.test.ts index 84484a2ae6d..74b43b08eaa 100644 --- a/apps/sim/lib/table/column-types/extension-points.test.ts +++ b/apps/sim/lib/table/column-types/extension-points.test.ts @@ -5,16 +5,14 @@ import { afterEach, describe, expect, it } from 'vitest' import { COLUMN_TYPE_REGISTRY, validateColumnTypeLimits, - valueForTypeConversion, wouldExceedColumnTypeLimit, } from '@/lib/table/column-types' import type { ColumnDefinition } from '@/lib/table/types' const definition = COLUMN_TYPE_REGISTRY.string const originalMaxPerTable = definition.maxPerTable -const originalValueForConversion = definition.valueForConversion -function restoreOptionalProperty(key: 'maxPerTable' | 'valueForConversion', value: unknown) { +function restoreOptionalProperty(key: 'maxPerTable', value: unknown) { if (value === undefined) { Reflect.deleteProperty(definition, key) return @@ -24,7 +22,6 @@ function restoreOptionalProperty(key: 'maxPerTable' | 'valueForConversion', valu afterEach(() => { restoreOptionalProperty('maxPerTable', originalMaxPerTable) - restoreOptionalProperty('valueForConversion', originalValueForConversion) }) describe('column type extension points', () => { @@ -40,40 +37,4 @@ describe('column type extension points', () => { `A table can have at most 1 ${definition.label} column`, ]) }) - - it('lets the source type normalize a value before conversion', () => { - Object.assign(definition, { - valueForConversion: (_value: unknown, target: ColumnDefinition) => - target.type === 'number' ? 42 : 'unchanged', - }) - - expect( - valueForTypeConversion( - 'stored-value', - { name: 'source', type: 'string' }, - { name: 'target', type: 'number' } - ) - ).toBe(42) - expect( - valueForTypeConversion( - 'stored-value', - { name: 'source', type: 'number' }, - { name: 'target', type: 'string' } - ) - ).toBe('stored-value') - }) - - it('preserves an intentional null from source normalization', () => { - Object.assign(definition, { - valueForConversion: () => null, - }) - - expect( - valueForTypeConversion( - 'stored-value', - { name: 'source', type: 'string' }, - { name: 'target', type: 'number' } - ) - ).toBeNull() - }) }) diff --git a/apps/sim/lib/table/column-types/import-coercion.ts b/apps/sim/lib/table/column-types/import-coercion.ts deleted file mode 100644 index 9bc0768c61e..00000000000 --- a/apps/sim/lib/table/column-types/import-coercion.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { parseTtlEpochSeconds } from '@/lib/table/column-types/ttl' -import type { ColumnType } from '@/lib/table/column-types/types' -import type { NormalizeDateCellOptions } from '@/lib/table/dates' -import type { JsonValue } from '@/lib/table/types' - -type ImportValue = Exclude -type ImportCoercer = (value: unknown, options?: NormalizeDateCellOptions) => ImportValue - -const IMPORT_COERCERS: Partial> = { - ttl: (value, options) => parseTtlEpochSeconds(value, options), -} - -/** Applies lightweight type-specific CSV coercion without loading the full column registry. */ -export function coerceColumnTypeImportValue( - type: ColumnType, - value: unknown, - options?: NormalizeDateCellOptions -): ImportValue | undefined { - return IMPORT_COERCERS[type]?.(value, options) -} diff --git a/apps/sim/lib/table/column-types/registry.server.ts b/apps/sim/lib/table/column-types/registry.server.ts index 5a6e23791ea..22b000f9660 100644 --- a/apps/sim/lib/table/column-types/registry.server.ts +++ b/apps/sim/lib/table/column-types/registry.server.ts @@ -84,11 +84,9 @@ const COERCED_WRITE_BACK_BATCH_SIZE = 5000 * Writes back the values a conversion's coercion produced. * * A retype is allowed exactly when the target type's `coerce` accepts the - * value, and `coerce` frequently *transforms* it — an epoch number becomes an - * ISO date, `$1,234.56` becomes `1234.56`. Without this, the cell keeps its old - * bytes under the new type, and since filters and sorts apply the type's - * `jsonbCast` to whatever is stored, an epoch left in a `date` column makes - * `::timestamptz` fail on EVERY query against that column. + * value, and `coerce` frequently transforms it — `$1,234.56` becomes `1234.56`. + * Without this, the cell keeps its old bytes under the new type, and the + * type's `jsonbCast` can fail on every filter or sort against that column. * * The values arrive already computed (the compatibility scan derived them), so * this is purely the write. It cannot be expressed set-based — the coercions diff --git a/apps/sim/lib/table/column-types/registry.ts b/apps/sim/lib/table/column-types/registry.ts index 4e4d0f82bc6..1f671a0df0a 100644 --- a/apps/sim/lib/table/column-types/registry.ts +++ b/apps/sim/lib/table/column-types/registry.ts @@ -76,6 +76,11 @@ export function columnTypeOf(column: Pick): ColumnType return COLUMN_TYPE_REGISTRY[column.type] ?? stringColumnType } +/** Compares equivalent values without changing their stored representation. */ +export function columnValueForEquality(value: JsonValue, column: ColumnDefinition): JsonValue { + return columnTypeOf(column).valueForEquality?.(value) ?? value +} + /** The definition for a type id, or `string`'s when the id is unknown. */ export function columnTypeById(type: string | undefined): ColumnTypeDefinition { return (isColumnType(type) && COLUMN_TYPE_REGISTRY[type]) || stringColumnType @@ -94,16 +99,6 @@ export function isValueCompatible(value: unknown, target: ColumnDefinition): boo return definition.coerce(value as JsonValue, target).ok } -/** Applies source-owned normalization before a value is converted to another type. */ -export function valueForTypeConversion( - value: JsonValue, - source: ColumnDefinition, - target: ColumnDefinition -): JsonValue { - const normalized = columnTypeOf(source).valueForConversion?.(value, target) - return normalized === undefined ? value : normalized -} - /** This type's own metadata errors; types carrying no metadata report none. */ export function validateTypeMetadata(column: ColumnDefinition): string[] { return columnTypeOf(column).validateDefinition?.(column) ?? [] diff --git a/apps/sim/lib/table/column-types/timestamp-sql.ts b/apps/sim/lib/table/column-types/timestamp-sql.ts new file mode 100644 index 00000000000..a6376816180 --- /dev/null +++ b/apps/sim/lib/table/column-types/timestamp-sql.ts @@ -0,0 +1,12 @@ +import { type SQL, sql } from 'drizzle-orm' +import type { TimestampValidation } from '@/lib/table/column-types/types' + +/** PostgreSQL 16+ validates the cast; the format and precision rules prevent implicit or rounded instants. */ +export function validatedTimestampSql(cell: SQL, validation: TimestampValidation): SQL { + return sql`CASE + WHEN ${cell} ~* ${validation.pattern} + AND pg_input_is_valid(${cell}, 'timestamptz') + AND COALESCE(length(substring(${cell} from '[.]([0-9]+)')), 0) <= ${validation.maxFractionDigits} + THEN (${cell})::timestamptz + END` +} diff --git a/apps/sim/lib/table/column-types/ttl.test.ts b/apps/sim/lib/table/column-types/ttl.test.ts index 717e96ebb8a..1b142d00c75 100644 --- a/apps/sim/lib/table/column-types/ttl.test.ts +++ b/apps/sim/lib/table/column-types/ttl.test.ts @@ -1,189 +1,168 @@ /** * @vitest-environment node */ - -import { describe, expect, it } from 'vitest' -import { - formatInstantInTimeZone, - getSupportedTimezones, - zonedWallClockToUtc, -} from '@/lib/core/utils/timezone' -import { parseTtlEpochSeconds, ttlColumnType } from '@/lib/table/column-types/ttl' +import { describe, expect, it, vi } from 'vitest' +import { isValueCompatible } from '@/lib/table/column-types' +import { ttlColumnType } from '@/lib/table/column-types/ttl' import { retypeCellRewrite } from '@/lib/table/columns/service' +import { + isTtlTimestamp, + normalizeTtlTimestamp, + TTL_FORMAT_ERROR, + todayAtTtlOffset, + ttlInstantForComparison, + ttlValueFromPicker, + ttlValueToPickerParts, +} from '@/lib/table/ttl-values' import type { ColumnDefinition, JsonValue } from '@/lib/table/types' +import { coerceRowToSchema } from '@/lib/table/validation' -const column = (over: Partial): ColumnDefinition => - ({ name: 'col', type: 'string', ...over }) as ColumnDefinition +const column: ColumnDefinition = { name: 'expires_at', type: 'ttl' } describe('TTL column type', () => { - it('converts epoch seconds to an ISO date before retyping', () => { - expect( - retypeCellRewrite(1_700_000_000, column({ type: 'date' }), column({ type: 'ttl' })) - ).toEqual({ value: '2023-11-14T22:13:20Z' }) - }) - - it('keeps blank and malformed TTL values out of the epoch-zero formatter', () => { - const cases: Array<[unknown, string]> = [ - [null, ''], - [undefined, ''], - ['', ''], - [' ', ' '], - [false, 'false'], - [[], ''], - ] - for (const [value, fallback] of cases) { - expect(ttlColumnType.formatForDisplay(value, column({ type: 'ttl' }))).toBe(fallback) - expect(ttlColumnType.formatForInput(value, column({ type: 'ttl' }))).toBe(fallback) - } - }) - - it('preserves blank and malformed TTL values when converting to a date', () => { - const target = column({ type: 'date' }) - const values: JsonValue[] = [null, '', ' ', false, []] - - for (const value of values) { - expect(ttlColumnType.valueForConversion?.(value, target)).toEqual(value) - } - }) - it.each([ - ['UTC', '2026-06-15T09:00:30', '2026-06-15T09:00:30.000Z'], - ['America/New_York', '2026-06-15T09:00:30', '2026-06-15T13:00:30.000Z'], - ['America/New_York', '2026-01-15T09:00:30', '2026-01-15T14:00:30.000Z'], - ['Asia/Kathmandu', '2026-06-15T09:00:30', '2026-06-15T03:15:30.000Z'], - ['Australia/Lord_Howe', '2026-06-15T09:00:30', '2026-06-14T22:30:30.000Z'], - ])('stores %s wall-clock input as the expected epoch second', (timezone, input, iso) => { - expect(parseTtlEpochSeconds(input, { timezone })).toBe(Date.parse(iso) / 1000) + '2026-09-07T14:30:00-07:00', + '2026-01-07T14:30:00-08:00', + '2026-09-07T14:30:00+05:45', + '2026-09-07T14:30:00+00:00', + '2024-02-29T23:59:59-00:00', + '0001-01-01T00:00:00-00:00', + '9999-12-31T23:59:59-00:00', + ])('stores and displays %s byte-for-byte', (value) => { + expect(isTtlTimestamp(value)).toBe(true) + expect(ttlColumnType.coerce(value, column)).toEqual({ ok: true, value }) + expect(ttlColumnType.validateCell(value, column)).toBeNull() + expect(ttlColumnType.formatForDisplay(value, column)).toBe(value) + expect(ttlColumnType.formatForInput(value, column)).toBe(value) + expect(isValueCompatible(value, column)).toBe(true) }) it.each([ - ['America/New_York', '2026-11-01T01:30', '2026-11-01T06:30:00.000Z'], - ['Europe/Berlin', '2026-10-25T02:30', '2026-10-25T01:30:00.000Z'], - ['Australia/Lord_Howe', '2026-04-05T01:45', '2026-04-04T15:15:00.000Z'], - ])( - 'chooses the later expiration when %s repeats a wall-clock time', - (timezone, input, laterInstant) => { - expect(parseTtlEpochSeconds(input, { timezone })).toBe(Date.parse(laterInstant) / 1000) - } - ) - - it.each([ - ['America/New_York', '2026-03-08T02:30', '2026-03-08T07:30:00.000Z'], - ['Europe/Berlin', '2026-03-29T02:30', '2026-03-29T01:30:00.000Z'], - ['Australia/Lord_Howe', '2026-10-04T02:15', '2026-10-03T15:45:00.000Z'], - ])( - 'moves a nonexistent %s wall-clock expiration forward across the gap', - (timezone, input, compatibleInstant) => { - expect(parseTtlEpochSeconds(input, { timezone })).toBe(Date.parse(compatibleInstant) / 1000) - } - ) - - it('rounds fractional instants up so expiration is never stored early', () => { - expect(parseTtlEpochSeconds('2023-11-14T22:13:20.001Z')).toBe(1_700_000_001) - expect(parseTtlEpochSeconds('2023-11-14T22:13:20.999Z')).toBe(1_700_000_001) - expect(parseTtlEpochSeconds('2023-11-14T22:13:20.0001Z')).toBe(1_700_000_001) - expect(parseTtlEpochSeconds('2023-11-14t22:13:20.001Z')).toBe(1_700_000_001) - expect(parseTtlEpochSeconds('2023-11-14t17:13:20.001', { timezone: 'America/New_York' })).toBe( - 1_700_000_001 + ['2026-09-07T14:30:00Z', '2026-09-07T14:30:00-00:00'], + ['2026-09-07T14:30Z', '2026-09-07T14:30:00-00:00'], + ['2026-09-07t14:30:00z', '2026-09-07T14:30:00-00:00'], + ['2026-09-07T14:30:00.000Z', '2026-09-07T14:30:00-00:00'], + ['2026-09-07T14:30:00.123400Z', '2026-09-07T14:30:00.1234-00:00'], + ['2026-09-07T07:30:00.000001-07:00', '2026-09-07T07:30:00.000001-07:00'], + ['2026-01-01T00:00:00.999999+01:00', '2026-01-01T00:00:00.999999+01:00'], + ['2026-11-01T01:30:00-04:00', '2026-11-01T01:30:00-04:00'], + ['2026-11-01T01:30:00-05:00', '2026-11-01T01:30:00-05:00'], + ])('preserves the clock and offset of %s without losing precision', (input, expected) => { + expect(isTtlTimestamp(input)).toBe(true) + expect(ttlColumnType.coerce(input, column)).toEqual({ ok: true, value: expected }) + expect(ttlColumnType.formatForDisplay(input, column)).toBe(expected) + expect(ttlColumnType.formatForInput(input, column)).toBe(expected) + expect(ttlColumnType.validateFilterValue?.(input, column)).toBeNull() + expect(normalizeTtlTimestamp(expected)).toBe(expected) + expect(retypeCellRewrite(input, column)).toEqual( + input === expected ? null : { value: expected } ) - expect(parseTtlEpochSeconds(new Date('2023-11-14T22:13:20.001Z'))).toBe(1_700_000_001) - expect(parseTtlEpochSeconds('2023-11-14T22:13:20.000Z')).toBe(1_700_000_000) }) - it('rounds historical sub-minute timezone offsets toward a later expiration', () => { - const timezone = 'Africa/Monrovia' - const exactInstant = Date.parse('1970-01-01T00:44:30Z') / 1000 - - expect(parseTtlEpochSeconds('1970-01-01T00:00:00', { timezone })).toBeGreaterThanOrEqual( - exactInstant - ) - - const editable = ttlColumnType.formatForInput(exactInstant, column({ type: 'ttl' }), { - timezone, - }) - expect(editable).toBe('1970-01-01T00:00:00-00:45') - expect(parseTtlEpochSeconds(editable, { timezone })).toBeGreaterThanOrEqual(exactInstant) - }) - - it('never resolves representative wall clocks early in any supported timezone', () => { - for (const timezone of getSupportedTimezones()) { - for (const wallClock of ['1970-01-01T00:00:00', '2026-06-15T09:00:30']) { - const exactSecond = Math.ceil( - zonedWallClockToUtc(wallClock, timezone, { ambiguousTime: 'later' }).getTime() / 1000 - ) - expect( - parseTtlEpochSeconds(wallClock, { timezone }), - `${timezone} ${wallClock}` - ).toBeGreaterThanOrEqual(exactSecond) - } - } - }) - - it('never moves stored epoch seconds earlier when formatted in any supported timezone', () => { - for (const timezone of getSupportedTimezones()) { - for (const seconds of [0, Date.parse('2026-11-01T06:30:00Z') / 1000]) { - const editable = ttlColumnType.formatForInput(seconds, column({ type: 'ttl' }), { - timezone, - }) - expect( - parseTtlEpochSeconds(editable, { timezone }), - `${timezone} ${editable}` - ).toBeGreaterThanOrEqual(seconds) - } + it('compares equivalent instants without rewriting stored offsets or dropping microseconds', () => { + const equal = [ + '2026-09-07T07:30:00.000001-07:00', + '2026-09-07T20:15:00.000001+05:45', + '2026-09-07T14:30:00.000001Z', + '2026-09-07T14:30:00.000001-00:00', + '2026-09-07T14:30:00.000001+00:00', + ] + for (const value of equal) { + expect(ttlColumnType.valueForEquality?.(value)).toBe('2026-09-07T14:30:00.000001Z') } - }) - - it('uses the timezone supplied for each call rather than a previous setting', () => { - const input = '2026-06-15T09:00:30' - - expect(parseTtlEpochSeconds(input, { timezone: 'America/New_York' })).toBe( - Date.parse('2026-06-15T13:00:30Z') / 1000 + expect(ttlInstantForComparison('2026-09-07T07:30:00.000002-07:00')).toBe( + '2026-09-07T14:30:00.000002Z' ) - expect(parseTtlEpochSeconds(input, { timezone: 'Asia/Kathmandu' })).toBe( - Date.parse('2026-06-15T03:15:30Z') / 1000 - ) - expect(parseTtlEpochSeconds(input, { timezone: 'America/New_York' })).toBe( - Date.parse('2026-06-15T13:00:30Z') / 1000 + expect(ttlInstantForComparison('2026-01-01T00:00:00.999999+01:00')).toBe( + '2025-12-31T23:00:00.999999Z' ) }) - it('round-trips the same epoch after the editor timezone changes', () => { - const seconds = Date.parse('2026-11-01T06:30:00Z') / 1000 - - for (const timezone of [ - 'UTC', - 'America/Los_Angeles', - 'America/New_York', - 'Asia/Kathmandu', - 'Australia/Lord_Howe', - ]) { - const editable = ttlColumnType.formatForInput(seconds, column({ type: 'ttl' }), { timezone }) - expect(editable).toBe(formatInstantInTimeZone(new Date(seconds * 1000), timezone)) - expect(parseTtlEpochSeconds(editable, { timezone })).toBe(seconds) - } + it.each([ + 1_700_000_000, + '1700000000', + new Date('2026-09-07T14:30:00Z'), + '2026-09-07', + '2026-09-07T14:30:00', + '2026-09-07T14:30:00+16:00', + '2026-09-07T14:30:00-07:60', + '2026-09-07T14:30:00.0000001Z', + '2026-09-07 14:30:00Z', + '2026-09-07T14:30:00Z ', + '2026-09-07T14:30:00Z\n', + '2026-09-07T14:30:00+0700', + '2026-09-07T14:30:00 America/Los_Angeles', + '2026-09-07T14:30:00Z[UTC]', + 'now', + 'today', + 'epoch', + 'infinity', + '-infinity', + '2026-02-29T12:00:00Z', + '2026-02-30T12:00:00Z', + '2026-02-30T12:00:00-07:00', + '2026-04-31T12:00:00Z', + '2026-09-07T24:00:00Z', + '2026-09-07T14:60:00Z', + '2026-09-07T14:30:60Z', + '0000-01-01T00:00:00Z', + '0001-01-01T00:00:00+01:00', + '9999-12-31T23:59:59-01:00', + '', + null, + false, + [], + {}, + ])('rejects ambiguous, invalid, or unsupported input %j', (value) => { + expect(isTtlTimestamp(value)).toBe(false) + expect(ttlColumnType.coerce(value, column)).toEqual({ ok: false }) + expect(ttlColumnType.validateCell(value, column)).toContain(TTL_FORMAT_ERROR) + expect(isValueCompatible(value, column)).toBe(false) }) - it('round-trips a low-year expiration through the editor', () => { - const input = '0050-01-15T12:00:00' - const seconds = parseTtlEpochSeconds(input, { timezone: 'UTC' }) + it('validates workflow/API writes and preserves absent or cleared expiration', () => { + const schema = { columns: [column] } + const valid = { expires_at: '2026-09-07T07:30:00-07:00' } + expect(coerceRowToSchema(valid, schema, 'reject').valid).toBe(true) + expect(valid.expires_at).toBe('2026-09-07T07:30:00-07:00') + expect(coerceRowToSchema({ expires_at: 1_700_000_000 }, schema, 'reject').valid).toBe(false) + expect(coerceRowToSchema({}, schema, 'reject').valid).toBe(true) + expect(coerceRowToSchema({ expires_at: null }, schema, 'reject').valid).toBe(true) + }) - expect(seconds).toBe(Date.parse(`${input}Z`) / 1000) - const editable = ttlColumnType.formatForInput(seconds, column({ type: 'ttl' }), { - timezone: 'UTC', + it('converts between TTL and text without rewriting the value', () => { + const value = '2026-09-07T14:30:00-00:00' + expect(retypeCellRewrite(value, { name: 'text', type: 'string' })).toBeNull() + expect(retypeCellRewrite(value, column)).toBeNull() + expect(retypeCellRewrite(value, { name: 'date', type: 'date' })).toEqual({ + value: '2026-09-07T14:30:00Z', }) - expect(editable).toBe(`${input}Z`) - expect(parseTtlEpochSeconds(editable, { timezone: 'UTC' })).toBe(seconds) }) - it('keeps the TTL repeated-hour policy separate from ordinary date behavior', () => { - const input = '2026-11-01T01:30' - const timezone = 'America/New_York' - - expect(zonedWallClockToUtc(input, timezone, { ambiguousTime: 'earlier' }).toISOString()).toBe( - '2026-11-01T05:30:00.000Z' - ) - expect(parseTtlEpochSeconds(input, { timezone })).toBe( - Date.parse('2026-11-01T06:30:00Z') / 1000 + it('serializes picker fields in their offset and defaults new values to -00:00', () => { + expect(ttlValueFromPicker('2026-09-07', '14:30')).toBe('2026-09-07T14:30:00-00:00') + expect(ttlValueFromPicker('2026-09-07', '14:30:45', '-07:00')).toBe('2026-09-07T14:30:45-07:00') + expect(ttlValueFromPicker('2026-09-07', null, '+05:45')).toBe('2026-09-07T00:00:00+05:45') + expect(ttlValueFromPicker('2026-09-07', '14:30:45.123456', '-08:00')).toBe( + '2026-09-07T14:30:45.123456-08:00' ) + expect(ttlValueToPickerParts('2026-09-07T07:30:45.123456-07:00')).toEqual({ + day: '2026-09-07', + time: '07:30:45.123456', + offset: '-07:00', + }) + expect(ttlValueToPickerParts('2026-09-07T07:30:00Z').offset).toBe('-00:00') + expect(ttlValueToPickerParts('')).toEqual({ day: null, time: null, offset: '-00:00' }) + }) + + it('calculates Today in the stored offset across a UTC date boundary', () => { + const now = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-01-01T01:00:00Z')) + try { + expect(todayAtTtlOffset('-07:00')).toBe('2025-12-31') + expect(todayAtTtlOffset('+05:45')).toBe('2026-01-01') + expect(todayAtTtlOffset('-00:00')).toBe('2026-01-01') + } finally { + now.mockRestore() + } }) }) diff --git a/apps/sim/lib/table/column-types/ttl.ts b/apps/sim/lib/table/column-types/ttl.ts index 5b04023e05c..975782a46b5 100644 --- a/apps/sim/lib/table/column-types/ttl.ts +++ b/apps/sim/lib/table/column-types/ttl.ts @@ -1,128 +1,52 @@ import { TypeTtl } from '@sim/emcn/icons' -import { formatInstantInTimeZone } from '@/lib/core/utils/timezone' import type { ColumnTypeDefinition } from '@/lib/table/column-types/types' import { - formatDateCellDisplay, - type NormalizeDateCellOptions, - normalizeDateCellValue, -} from '@/lib/table/dates' -import type { ColumnDefinition } from '@/lib/table/types' - -const NUMERIC_VALUE_PATTERN = /^-?\d+(?:\.\d+)?$/ -const ISO_DATE_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})(?:$|[T ])/i -const FRACTIONAL_SECONDS_PATTERN = /[T ]\d{1,2}:\d{2}:\d{2}\.(\d+)/i - -function isRepresentableEpochSeconds(value: number): boolean { - return Number.isSafeInteger(value) && !Number.isNaN(new Date(value * 1000).getTime()) -} - -/** Rounds toward the future so integer-second storage can never expire an instant early. */ -function epochSecondAtOrAfter(milliseconds: number): number { - return Math.ceil(milliseconds / 1000) -} - -/** Whether an ISO-shaped input names any instant after its whole second. */ -function hasFractionalSecond(value: string): boolean { - const digits = value.match(FRACTIONAL_SECONDS_PATTERN)?.[1] - return digits ? /[1-9]/.test(digits) : false -} - -/** Converts a TTL cell input to integer Unix epoch seconds. */ -export function parseTtlEpochSeconds( - value: unknown, - options?: NormalizeDateCellOptions -): number | null { - if (typeof value === 'number') return isRepresentableEpochSeconds(value) ? value : null - - if (value instanceof Date) { - const milliseconds = value.getTime() - return Number.isNaN(milliseconds) ? null : epochSecondAtOrAfter(milliseconds) - } - - if (typeof value !== 'string') return null - const trimmed = value.trim() - if (!trimmed) return null - - if (NUMERIC_VALUE_PATTERN.test(trimmed)) { - const numeric = Number(trimmed) - return isRepresentableEpochSeconds(numeric) ? numeric : null - } - - const ttlOptions: NormalizeDateCellOptions = { - ...options, - ambiguousTime: 'later', - offsetMinuteRounding: 'floor', - } - const normalized = normalizeDateCellValue(trimmed, ttlOptions) - if (normalized === null) return null - const instant = /^\d{4}-\d{2}-\d{2}$/.test(normalized) - ? normalizeDateCellValue(`${normalized}T00:00:00`, ttlOptions) - : normalized - if (instant === null) return null - const inputIsoDate = trimmed.match(ISO_DATE_PREFIX_PATTERN)?.[1] - if (inputIsoDate && instant.slice(0, 10) !== inputIsoDate) return null - const milliseconds = Date.parse(instant) + (hasFractionalSecond(trimmed) ? 1 : 0) - if (Number.isNaN(milliseconds)) return null - const seconds = epochSecondAtOrAfter(milliseconds) - return isRepresentableEpochSeconds(seconds) ? seconds : null -} - -function epochSecondsToIso(value: unknown): string | null { - if ( - typeof value !== 'number' && - (typeof value !== 'string' || !NUMERIC_VALUE_PATTERN.test(value.trim())) - ) { - return null - } - const seconds = typeof value === 'number' ? value : Number(value) - if (!isRepresentableEpochSeconds(seconds)) return null - return new Date(seconds * 1000).toISOString().replace('.000Z', 'Z') -} - -function epochSecondsToEditable(value: unknown, timeZone?: string): string | null { - const iso = epochSecondsToIso(value) - if (!iso || !timeZone) return iso - return formatInstantInTimeZone(new Date(iso), timeZone, { offsetMinuteRounding: 'floor' }) -} + isTtlTimestamp, + normalizeTtlTimestamp, + TTL_FORMAT_ERROR, + TTL_TIMESTAMP_VALIDATION, + ttlInstantForComparison, +} from '@/lib/table/ttl-values' export const ttlColumnType: ColumnTypeDefinition = { id: 'ttl', label: 'Expiration', maxPerTable: 1, icon: TypeTtl, - jsonbCast: 'numeric', + jsonbCast: 'timestamptz', + timestampValidation: TTL_TIMESTAMP_VALIDATION, storesOpaqueIds: false, supportsUnique: true, - sampleValue: 1_706_659_200, + sampleValue: '2024-01-31T00:00:00-00:00', ownedMetadata: [], - workflowInputType: 'number', - editor: 'date', + workflowInputType: 'string', + editor: 'offset-date', expandable: false, - typeaheadPattern: /[\d\-/]/, - parseErrorMessage: 'Invalid expiration date', + typeaheadPattern: /\d/, + parseErrorMessage: TTL_FORMAT_ERROR, - coerce(value, _column, context) { - const seconds = parseTtlEpochSeconds(value, context) - return seconds === null ? { ok: false } : { ok: true, value: seconds } + coerce(value) { + const normalized = normalizeTtlTimestamp(value) + return normalized === null ? { ok: false } : { ok: true, value: normalized } }, - valueForConversion(value, target: ColumnDefinition) { - if (target.type !== 'date') return value - return epochSecondsToIso(value) ?? value + valueForEquality(value) { + return ttlInstantForComparison(value) ?? value }, validateCell(value, column) { - return typeof value === 'number' && isRepresentableEpochSeconds(value) - ? null - : `${column.name} must be valid epoch seconds` + return isTtlTimestamp(value) ? null : `${column.name}: ${TTL_FORMAT_ERROR}` + }, + + validateFilterValue(value, column) { + return isTtlTimestamp(value) ? null : `${column.name}: ${TTL_FORMAT_ERROR}` }, formatForDisplay(value) { - const iso = epochSecondsToIso(value) - return iso === null ? String(value ?? '') : formatDateCellDisplay(iso, { seconds: true }) + return normalizeTtlTimestamp(value) ?? String(value ?? '') }, - formatForInput(value, _column, context) { - return epochSecondsToEditable(value, context?.timezone) ?? String(value ?? '') + formatForInput(value) { + return normalizeTtlTimestamp(value) ?? String(value ?? '') }, } diff --git a/apps/sim/lib/table/column-types/types.ts b/apps/sim/lib/table/column-types/types.ts index 72edeead0d0..ce34e24e356 100644 --- a/apps/sim/lib/table/column-types/types.ts +++ b/apps/sim/lib/table/column-types/types.ts @@ -20,7 +20,6 @@ */ import type React from 'react' -import type { NormalizeDateCellOptions } from '@/lib/table/dates' import type { ColumnDefinition, JsonValue } from '@/lib/table/types' /** @@ -50,6 +49,8 @@ export type ColumnCellEditor = | 'text' /** Calendar + time picker. */ | 'date' + /** Calendar + time picker retaining the cell's numeric offset, independent of viewer settings. */ + | 'offset-date' /** Option dropdown. */ | 'select' /** Not editable inline — the grid toggles it in place instead. */ @@ -69,6 +70,12 @@ export type TypeSpecificColumnKey = (typeof TYPE_SPECIFIC_COLUMN_KEYS)[number] /** Result of coercing a raw value toward a column's declared type. */ export type CoerceResult = { ok: true; value: JsonValue } | { ok: false } +/** Additional format and precision rules for native PostgreSQL timestamp validation. */ +export interface TimestampValidation { + readonly pattern: string + readonly maxFractionDigits: number +} + export interface ColumnTypeDefinition { readonly id: ColumnType @@ -83,6 +90,8 @@ export interface ColumnTypeDefinition { * comparison is correct. Single source for both filter ranges and sort order. */ readonly jsonbCast: 'numeric' | 'timestamptz' | null + /** Guards timestamp comparisons against malformed stored cells without guessing a timezone. */ + readonly timestampValidation?: TimestampValidation /** * Wire operators a column of this type accepts, or `null` for "all @@ -161,18 +170,17 @@ export interface ColumnTypeDefinition { * implementation — the server calls it before persisting and the grid calls * it to fill the optimistic cache, so the two can no longer disagree. */ - coerce( - value: JsonValue, - column: ColumnDefinition, - context?: NormalizeDateCellOptions - ): CoerceResult + coerce(value: JsonValue, column: ColumnDefinition): CoerceResult - /** Source-owned normalization applied before checking or rewriting a type conversion. */ - valueForConversion?(value: JsonValue, target: ColumnDefinition): JsonValue + /** Equivalent-value projection for in-memory equality; also enables jsonbCast for SQL equality. */ + valueForEquality?(value: JsonValue): JsonValue /** Validates a stored cell's shape. Returns an error message, or null when valid. */ validateCell(value: JsonValue, column: ColumnDefinition): string | null + /** Optional strict validation for non-null equality, membership, and range operands. */ + validateFilterValue?(value: JsonValue, column: ColumnDefinition): string | null + /** * Validates this type's own column metadata (a `select`'s options, a * `currency`'s code). Omitted by types that carry none. @@ -214,11 +222,7 @@ export interface ColumnTypeDefinition { formatForDisplay(value: unknown, column: ColumnDefinition): string /** Stored value → the text an editor input starts with. */ - formatForInput( - value: unknown, - column: ColumnDefinition, - context?: NormalizeDateCellOptions - ): string + formatForInput(value: unknown, column: ColumnDefinition): string /** * Metadata stamped onto a newly created column of this type, so the schema diff --git a/apps/sim/lib/table/columns/retype-cell.test.ts b/apps/sim/lib/table/columns/retype-cell.test.ts index b1b6a74d888..479563fc74b 100644 --- a/apps/sim/lib/table/columns/retype-cell.test.ts +++ b/apps/sim/lib/table/columns/retype-cell.test.ts @@ -2,25 +2,13 @@ * @vitest-environment node */ -import { afterEach, describe, expect, it } from 'vitest' -import { COLUMN_TYPE_REGISTRY } from '@/lib/table/column-types' +import { describe, expect, it } from 'vitest' import { retypeCellRewrite } from '@/lib/table/columns/service' import type { ColumnDefinition } from '@/lib/table/types' const column = (over: Partial): ColumnDefinition => ({ name: 'col', type: 'string', ...over }) as ColumnDefinition -const sourceDefinition = COLUMN_TYPE_REGISTRY.string -const originalValueForConversion = sourceDefinition.valueForConversion - -afterEach(() => { - if (originalValueForConversion === undefined) { - Reflect.deleteProperty(sourceDefinition, 'valueForConversion') - return - } - Object.assign(sourceDefinition, { valueForConversion: originalValueForConversion }) -}) - describe('retypeCellRewrite', () => { it('preserves an empty string the target type can hold', () => { // `''` is a real stored value: `coerceRowValues` keeps it for `string`, and @@ -44,29 +32,6 @@ describe('retypeCellRewrite', () => { expect(retypeCellRewrite('true', column({ type: 'boolean' }))).toEqual({ value: true }) }) - it('writes back null produced by source normalization', () => { - Object.assign(sourceDefinition, { valueForConversion: () => null }) - - expect( - retypeCellRewrite('stored-value', column({ type: 'number' }), column({ type: 'string' })) - ).toEqual({ value: null }) - }) - - it('coerces source-normalized values into select storage', () => { - Object.assign(sourceDefinition, { valueForConversion: () => 'Choice' }) - - expect( - retypeCellRewrite( - 'stored-value', - column({ - type: 'select', - options: [{ id: 'opt_choice', name: 'Choice' }], - }), - column({ type: 'string' }) - ) - ).toEqual({ value: 'opt_choice' }) - }) - it('skips a cell whose stored value already matches the coercion', () => { expect(retypeCellRewrite('kept', column({ type: 'json' }))).toBeNull() expect(retypeCellRewrite(3, column({ type: 'json' }))).toBeNull() diff --git a/apps/sim/lib/table/columns/service.ts b/apps/sim/lib/table/columns/service.ts index fa0a274d146..24469f7cf96 100644 --- a/apps/sim/lib/table/columns/service.ts +++ b/apps/sim/lib/table/columns/service.ts @@ -27,8 +27,8 @@ import { columnTypeOf, isValueCompatible, TYPE_SPECIFIC_COLUMN_KEYS, - valueForTypeConversion, } from '@/lib/table/column-types' +import { columnTextForEquality } from '@/lib/table/column-types/comparison-sql' import { migrationFrom, migrationTo, @@ -657,7 +657,7 @@ async function applyConstraints( `Cannot set column "${column.name}" as unique: ${column.type} columns compare stored values that would allow only one row per value.` ) } - if (await hasDuplicateValues(trx, tableId, workspaceId, columnKey)) { + if (await hasDuplicateValues(trx, tableId, workspaceId, column)) { throw new OrchestrationError( 'validation', `Cannot set column "${column.name}" as unique: duplicate values exist` @@ -692,7 +692,7 @@ async function persistColumns( } /** - * Whether any two rows share a stored value in this column. + * Whether any two rows share an equal value in this column. * * Shared by the constraint write and the retype's pre-validation so the two * cannot drift — the same reason {@link countEmptyCells} is shared. A retype @@ -704,10 +704,13 @@ async function hasDuplicateValues( trx: DbTransaction, tableId: string, workspaceId: string, - columnKey: string + column: ColumnDefinition ): Promise { + const columnKey = getColumnId(column) + const storedValue = sql`${userTableRows.data}->>${columnKey}::text` + const comparableValue = columnTextForEquality(storedValue, column) const duplicates = (await trx.execute( - sql`SELECT ${userTableRows.data}->>${columnKey}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${tableId} AND workspace_id = ${workspaceId} AND ${userTableRows.data} ? ${columnKey} AND ${userTableRows.data}->>${columnKey}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1` + sql`SELECT ${comparableValue} AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${tableId} AND workspace_id = ${workspaceId} AND ${userTableRows.data} ? ${columnKey} AND ${comparableValue} IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1` )) as { val: string; cnt: number }[] return duplicates.length > 0 } @@ -763,24 +766,17 @@ export function applyPendingRename( * (`countEmptyCells` does not treat `''` as empty). * * Everything else goes through the target's `coerce`, which frequently - * *transforms* the value — an epoch becomes an ISO date, `$1,234.56` becomes - * `1234.56`. Without writing the transformed value back the cell keeps its old - * bytes under the new type, and since filters and sorts apply the type's - * `jsonbCast` to whatever is stored, an epoch left in a `date` column makes - * `::timestamptz` fail on EVERY query against that column. + * transforms the value — `$1,234.56` becomes `1234.56`. Without writing the + * transformed value back, the cell keeps its old bytes under the new type, + * and the type's `jsonbCast` can fail on every filter or sort. */ export function retypeCellRewrite( value: unknown, - target: ColumnDefinition, - source?: ColumnDefinition + target: ColumnDefinition ): { value: JsonValue } | null { if (value === null || value === undefined) return null - const effective = source - ? valueForTypeConversion(value as JsonValue, source, target) - : (value as JsonValue) - - if (effective === null) return { value: null } + const effective = value as JsonValue if (!isValueCompatibleWithColumn(effective, target)) { // Incompatible non-blanks never reach here: the compatibility scan already @@ -926,7 +922,6 @@ export async function updateColumnType( const isSelectType = data.newType === 'select' const targetOptions = data.options ?? column.options ?? [] const targetMultiple = data.multiple ?? column.multiple - const sourceNormalizesConversion = columnTypeOf(column).valueForConversion !== undefined // Leaving `select` behind: stored cells hold option ids, which mean nothing // once the column is text/number/etc. Check compatibility against the option // NAME — that's what the cell will actually become (migrated below). @@ -992,7 +987,7 @@ export async function updateColumnType( const effective = convertingAwayFromSelect ? selectValueForConversion(column, value) - : valueForTypeConversion(value as JsonValue, column, convertedColumn) + : value if (!isValueCompatibleWithColumn(effective, convertedColumn)) { if (effective === null || effective === '') { @@ -1043,7 +1038,7 @@ export async function updateColumnType( resolved: new Map(), } await migrationFrom(column.type)?.(migrationContext) - if (!isSelectType || sourceNormalizesConversion) { + if (!isSelectType) { let rewriteAfterId: string | undefined while (true) { const rows = await readColumnRetypePage( @@ -1057,7 +1052,7 @@ export async function updateColumnType( if (rows.length === 0) break const coercedByRowId = new Map() for (const row of rows) { - const rewrite = retypeCellRewrite(row.value, convertedColumn, column) + const rewrite = retypeCellRewrite(row.value, convertedColumn) if (rewrite) coercedByRowId.set(row.id, rewrite.value) } await writeBackCoercedCells( @@ -1083,7 +1078,7 @@ export async function updateColumnType( // report an error with the retype already committed and the original text // irrecoverably rewritten. if (data.unique === true && !column.unique) { - if (await hasDuplicateValues(trx, data.tableId, table.workspaceId, columnKey)) { + if (await hasDuplicateValues(trx, data.tableId, table.workspaceId, convertedColumn)) { throw new OrchestrationError( 'validation', `Cannot change column "${column.name}" to type "${data.newType}" and set it as unique: the converted values contain duplicates.` diff --git a/apps/sim/lib/table/dates.ts b/apps/sim/lib/table/dates.ts index f112ad2f242..57cb150f810 100644 --- a/apps/sim/lib/table/dates.ts +++ b/apps/sim/lib/table/dates.ts @@ -26,7 +26,6 @@ import { formatIsoYear, formatUtcOffsetSuffix, - type ZonedWallClockOptions, zonedWallClockWithOffset, } from '@/lib/core/utils/timezone' @@ -266,14 +265,6 @@ export interface NormalizeDateCellOptions { * zone. */ timezone?: string - /** - * Which instant to use when a naive wall time occurs twice during a DST - * fall-back. Ordinary date cells preserve their historical earlier-instant - * behavior; instant-like callers may explicitly choose `later`. - */ - ambiguousTime?: ZonedWallClockOptions['ambiguousTime'] - /** How sub-minute historical offsets are serialized to RFC 3339 minutes. */ - offsetMinuteRounding?: ZonedWallClockOptions['offsetMinuteRounding'] } /** @@ -335,8 +326,7 @@ export function normalizeDateCellValue( const wallClock = isoWallClock ?? localizedWallClock ?? parseNaiveWallClockAsUtc(trimmed) if (!wallClock) return null return zonedWallClockWithOffset(wallClock, options.timezone, { - ambiguousTime: options.ambiguousTime ?? 'earlier', - offsetMinuteRounding: options.offsetMinuteRounding, + ambiguousTime: 'earlier', }) } return formatLocalFieldsAsWall(parsed, -parsed.getTimezoneOffset()) diff --git a/apps/sim/lib/table/import.test.ts b/apps/sim/lib/table/import.test.ts index f8259213a45..e41816812a3 100644 --- a/apps/sim/lib/table/import.test.ts +++ b/apps/sim/lib/table/import.test.ts @@ -173,25 +173,26 @@ describe('import', () => { expect(coerceValue('not-a-date', 'date')).toBe('not-a-date') }) - it('coerces TTL imports to epoch seconds and rejects invalid input', () => { - expect(coerceValue('2023-11-14T22:13:20Z', 'ttl')).toBe(1_700_000_000) - expect(coerceValue('1700000000', 'ttl')).toBe(1_700_000_000) - expect(coerceValue('2023-11-14 17:13:20', 'ttl', { timezone: 'America/New_York' })).toBe( - 1_700_000_000 - ) - expect(coerceValue('not-a-date', 'ttl')).toBeNull() - }) - - it('applies the timezone supplied to each TTL import independently', () => { - const input = '2026-06-15 09:00:30' - - expect(coerceValue(input, 'ttl', { timezone: 'America/New_York' })).toBe( - Date.parse('2026-06-15T13:00:30Z') / 1000 - ) - expect(coerceValue(input, 'ttl', { timezone: 'Asia/Kathmandu' })).toBe( - Date.parse('2026-06-15T03:15:30Z') / 1000 - ) - expect(coerceValue('2023-11-14T22:13:20.001Z', 'ttl')).toBe(1_700_000_001) + it('preserves explicit TTL offsets regardless of the import timezone', () => { + const input = '2026-06-15T09:00:30Z' + for (const timezone of ['UTC', 'America/New_York', 'Asia/Kathmandu']) { + expect(coerceValue(input, 'ttl', { timezone })).toBe('2026-06-15T09:00:30-00:00') + expect(coerceValue('2026-06-15T02:00:30-07:00', 'ttl', { timezone })).toBe( + '2026-06-15T02:00:30-07:00' + ) + expect(coerceValue('2026-06-15T09:00:30.123456Z', 'ttl', { timezone })).toBe( + '2026-06-15T09:00:30.123456-00:00' + ) + for (const invalid of [ + '1700000000', + '2026-06-15 09:00:30', + '2026-06-15T09:00:30+24:00', + '2026-06-15T09:00:30.0000001Z', + 'not-a-date', + ]) { + expect(coerceValue(invalid, 'ttl', { timezone })).toBeNull() + } + } }) }) diff --git a/apps/sim/lib/table/import.ts b/apps/sim/lib/table/import.ts index 50ea7cfc3b2..9e71e66d345 100644 --- a/apps/sim/lib/table/import.ts +++ b/apps/sim/lib/table/import.ts @@ -15,9 +15,9 @@ import type { Options as CsvParseOptions } from 'csv-parse' import { OrchestrationError } from '@/lib/core/orchestration/types' import { getColumnId } from '@/lib/table/column-keys' import type { ColumnType } from '@/lib/table/column-types' -import { coerceColumnTypeImportValue } from '@/lib/table/column-types/import-coercion' import { parseCurrencyInput } from '@/lib/table/currency' import { type NormalizeDateCellOptions, normalizeDateCellValue } from '@/lib/table/dates' +import { normalizeTtlTimestamp } from '@/lib/table/ttl-values' import type { ColumnDefinition, RowData, TableSchema } from '@/lib/table/types' import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types' @@ -471,8 +471,8 @@ export function inferSchemaFromCsv( * * Deliberately not routed through the column-type registry: its contract is * "coerced or rejected", while an import needs invalid raw text to survive so - * row-level validation can name it. Type-specific import behavior uses a - * lightweight capability map so CSV clients do not load the full registry. + * row-level validation can name it. Lightweight parsers keep the full + * column registry out of CSV clients. */ export function coerceValue( value: unknown, @@ -481,10 +481,9 @@ export function coerceValue( ): string | number | boolean | null | Record | unknown[] { if (value === null || value === undefined || value === '') return null - const typeSpecificValue = coerceColumnTypeImportValue(colType, value, options) - if (typeSpecificValue !== undefined) return typeSpecificValue - switch (colType) { + case 'ttl': + return normalizeTtlTimestamp(value) case 'number': { const n = Number(value) return Number.isNaN(n) ? null : n diff --git a/apps/sim/lib/table/orchestration/import.test.ts b/apps/sim/lib/table/orchestration/import.test.ts index 1212f5795e6..8cea50560b2 100644 --- a/apps/sim/lib/table/orchestration/import.test.ts +++ b/apps/sim/lib/table/orchestration/import.test.ts @@ -337,7 +337,7 @@ describe('performTableCsvImport', () => { rejectedSamples: [], }) expect(mockImportAppendRows.mock.calls[0][2]).toEqual([ - { col_expires_at: 1_700_000_000 }, + { col_expires_at: '2023-11-14T22:13:20-00:00' }, { col_expires_at: null }, ]) }) diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index eb047b0fb2c..1bbd2e96418 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -24,7 +24,7 @@ import { wouldExceedRowLimit, } from '@/lib/table/billing' import { getColumnId } from '@/lib/table/column-keys' -import { columnTypeOf } from '@/lib/table/column-types' +import { columnTypeOf, columnValueForEquality } from '@/lib/table/column-types' import { getMaxPageBytes, TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' import { TableQueryValidationError } from '@/lib/table/errors' import { @@ -537,7 +537,8 @@ export async function replaceTableRowsWithTx( const value = row[colId] if (value === null || value === undefined) continue // Case-sensitive, consistent with the unique-constraint check leaf. - const normalized = typeof value === 'string' ? value : JSON.stringify(value) + const comparable = columnValueForEquality(value, col) + const normalized = typeof comparable === 'string' ? comparable : JSON.stringify(comparable) const map = seen.get(colId)! if (map.has(normalized)) { throw new OrchestrationError( diff --git a/apps/sim/lib/table/sql.ts b/apps/sim/lib/table/sql.ts index a189588a134..d154a6f44db 100644 --- a/apps/sim/lib/table/sql.ts +++ b/apps/sim/lib/table/sql.ts @@ -19,6 +19,7 @@ import { SINGLE_SELECT_OPERATORS, SINGLE_SELECT_OPS, } from '@/lib/table/column-types' +import { columnTextForEquality } from '@/lib/table/column-types/comparison-sql' import { NAME_PATTERN } from '@/lib/table/constants' import { normalizeDateCellValue } from '@/lib/table/dates' import { TableQueryValidationError } from '@/lib/table/errors' @@ -364,7 +365,7 @@ function validateComparisonValue( `Range operator on column "${label}" (date) requires a date string, got ${typeof value}` ) } - if (normalizeDateCellValue(value) === null) { + if (!columnTypeById(columnType).coerce(value, { name: label, type: columnType ?? 'date' }).ok) { throw new TableQueryValidationError( `Range operator on column "${label}" (date) requires a parseable date string, got "${truncate(value, 64)}"` ) @@ -540,7 +541,8 @@ function buildFieldCondition( * matching the legacy behavior of emitting no clause. * * Equality (`eq`/`ne`/`in`/`nin`) uses case-sensitive JSONB containment (GIN - * indexed). Text matches (`contains`/`ncontains`/`startsWith`/`endsWith`) are + * indexed), except types with an equality projection, which use their database + * cast. Text matches (`contains`/`ncontains`/`startsWith`/`endsWith`) are * ILIKE (case-insensitive). Ranges cast per column type. */ export function fieldPredicate( @@ -560,6 +562,18 @@ export function fieldPredicate( } const columnType = column?.type + const validateFilterValue = column && columnTypeOf(column).validateFilterValue + if ( + column && + validateFilterValue && + ['eq', 'ne', 'in', 'nin', 'gt', 'gte', 'lt', 'lte'].includes(op) + ) { + for (const operand of Array.isArray(value) ? value : [value]) { + if (operand === null) continue + const error = validateFilterValue(operand as JsonValue, column) + if (error) throw new TableQueryValidationError(error) + } + } // Messages must name what the CALLER sent. `field` is the storage key by the // time it reaches here (the boundaries translate name → id before building // SQL), so a raw `field` reports a `col_…` the caller never supplied. @@ -614,12 +628,21 @@ export function fieldPredicate( : coerceContainmentOperand(column, value as JsonValue) : value + const equalityClause = (operand: JsonValue): SQL => { + const definition = column && columnTypeOf(column) + if (column && operand !== null && definition?.valueForEquality && definition.jsonbCast) { + const cell = columnTextForEquality(sql.raw(`${tableName}.data->>'${field}'`), column) + return sql`COALESCE(${cell} = ${operand}::${sql.raw(definition.jsonbCast)}, false)` + } + return buildContainmentClause(tableName, field, operand) + } + switch (op) { case 'eq': - return buildContainmentClause(tableName, field, containmentValue as JsonValue) + return equalityClause(containmentValue as JsonValue) case 'ne': - return sql`NOT (${buildContainmentClause(tableName, field, containmentValue as JsonValue)})` + return sql`NOT (${equalityClause(containmentValue as JsonValue)})` case 'gt': return buildComparisonClause(tableName, field, column, '>', value as number | string) @@ -633,17 +656,15 @@ export function fieldPredicate( case 'in': { const values = containmentValue if (!Array.isArray(values) || values.length === 0) return undefined - if (values.length === 1) return buildContainmentClause(tableName, field, values[0]) - const inConditions = values.map((v) => buildContainmentClause(tableName, field, v)) + if (values.length === 1) return equalityClause(values[0]) + const inConditions = values.map(equalityClause) return sql`(${sql.join(inConditions, sql.raw(' OR '))})` } case 'nin': { const values = containmentValue if (!Array.isArray(values) || values.length === 0) return undefined - const ninConditions = values.map( - (v) => sql`NOT (${buildContainmentClause(tableName, field, v)})` - ) + const ninConditions = values.map((v) => sql`NOT (${equalityClause(v)})`) return sql`(${sql.join(ninConditions, sql.raw(' AND '))})` } diff --git a/apps/sim/lib/table/ttl-values.ts b/apps/sim/lib/table/ttl-values.ts new file mode 100644 index 00000000000..96896ab95f0 --- /dev/null +++ b/apps/sim/lib/table/ttl-values.ts @@ -0,0 +1,88 @@ +import { z } from 'zod' + +export const TTL_FORMAT_ERROR = + 'Expiration must be an ISO timestamp with Z or an explicit UTC offset (for example, 2026-09-07T14:30:00-07:00), with at most 6 fractional second digits' + +/** Zod owns the ISO format; SQL also checks native timestamptz validity before casting. */ +export const TTL_TIMESTAMP_VALIDATION = { + pattern: z.regexes.datetime({ offset: true }).source, + maxFractionDigits: 6, +} as const + +const timestampSchema = z.iso.datetime({ offset: true }) + +function parseTtlTimestamp(value: unknown) { + if (typeof value !== 'string' || value !== value.trim()) return null + const result = timestampSchema.safeParse(value.toUpperCase()) + if (!result.success) return null + + const timestamp = result.data + const offset = timestamp.endsWith('Z') ? 'Z' : timestamp.slice(-6) + const [dateTime, fractionalSecond = ''] = timestamp.slice(0, -offset.length).split('.') + if ( + timestamp.startsWith('0000-') || + (offset !== 'Z' && Number(offset.slice(1, 3)) > 15) || + fractionalSecond.length > TTL_TIMESTAMP_VALIDATION.maxFractionDigits + ) { + return null + } + + const wallClock = dateTime.length === 16 ? `${dateTime}:00` : dateTime + const instant = new Date(`${wallClock}${offset}`) + if ( + !Number.isFinite(instant.getTime()) || + instant.getUTCFullYear() < 1 || + instant.getUTCFullYear() > 9999 + ) { + return null + } + const fraction = fractionalSecond.replace(/0+$/, '') + return { + wallClock, + fraction: fraction ? `.${fraction}` : '', + offset: offset === 'Z' ? '-00:00' : offset, + instant, + } +} + +/** Preserves the supplied clock and offset, spelling Z as -00:00, without losing microseconds. */ +export function normalizeTtlTimestamp(value: unknown): string | null { + const parsed = parseTtlTimestamp(value) + return parsed ? `${parsed.wallClock}${parsed.fraction}${parsed.offset}` : null +} + +/** An instant-only comparison value; never used to rewrite the stored offset. */ +export function ttlInstantForComparison(value: unknown): string | null { + const parsed = parseTtlTimestamp(value) + return parsed ? `${parsed.instant.toISOString().slice(0, -5)}${parsed.fraction}Z` : null +} + +/** Whether a value names a real instant with an explicit offset. */ +export function isTtlTimestamp(value: unknown): value is string { + return normalizeTtlTimestamp(value) !== null +} + +/** Serializes picker fields in their existing offset. A day alone means midnight in that offset. */ +export function ttlValueFromPicker(day: string, time: string | null, offset = '-00:00'): string { + const seconds = time ? (time.length === 5 ? `${time}:00` : time) : '00:00:00' + return `${day}T${seconds}${offset}` +} + +/** Reads the stored clock and offset for the picker without converting the instant. */ +export function ttlValueToPickerParts(value: string): { + day: string | null + time: string | null + offset: string +} { + const normalized = normalizeTtlTimestamp(value) + return normalized + ? { day: normalized.slice(0, 10), time: normalized.slice(11, -6), offset: normalized.slice(-6) } + : { day: null, time: null, offset: '-00:00' } +} + +/** Today's calendar date in a fixed numeric offset, independent of profile timezone settings. */ +export function todayAtTtlOffset(offset: string): string { + const minutes = Number(offset.slice(1, 3)) * 60 + Number(offset.slice(4, 6)) + const signedMinutes = offset.startsWith('-') ? -minutes : minutes + return new Date(Date.now() + signedMinutes * 60_000).toISOString().slice(0, 10) +} diff --git a/apps/sim/lib/table/validation.ts b/apps/sim/lib/table/validation.ts index e5fd89c3d2d..c32d551e20d 100644 --- a/apps/sim/lib/table/validation.ts +++ b/apps/sim/lib/table/validation.ts @@ -12,6 +12,7 @@ import { COLUMN_TYPE_REGISTRY, COLUMN_TYPES, columnTypeOf, + columnValueForEquality, isColumnType, TYPE_SPECIFIC_COLUMN_KEYS, validateColumnTypeLimits, @@ -423,7 +424,11 @@ export function validateUniqueConstraints( const duplicate = existingRows.find((row) => { if (excludeRowId && row.id === excludeRowId) return false // Case-sensitive, matching the DB unique-check leaf (`fieldPredicate` eq). - return value === row.data[key] + const existing = row.data[key] + return ( + existing !== undefined && + columnValueForEquality(value, column) === columnValueForEquality(existing, column) + ) }) if (duplicate) { @@ -570,7 +575,7 @@ export async function checkBatchUniqueConstraintsDb( const value = rowData[key] if (value === null || value === undefined) continue - const normalizedValue = JSON.stringify(value) + const normalizedValue = JSON.stringify(columnValueForEquality(value, column)) // Check for duplicate within batch const columnValueMap = batchValueMap.get(key)! @@ -640,7 +645,7 @@ export async function checkBatchUniqueConstraintsDb( // Map conflicts back to batch rows for (const conflict of conflictingRows) { const conflictData = conflict.data as RowData - const conflictValue = conflictData[columnId] + const conflictValue = columnValueForEquality(conflictData[columnId], column) const normalizedConflictValue = typeof conflictValue === 'string' ? conflictValue : JSON.stringify(conflictValue) @@ -649,8 +654,11 @@ export async function checkBatchUniqueConstraintsDb( const rowValue = rows[i][columnId] if (rowValue === null || rowValue === undefined) continue + const comparableRowValue = columnValueForEquality(rowValue, column) const normalizedRowValue = - typeof rowValue === 'string' ? rowValue : JSON.stringify(rowValue) + typeof comparableRowValue === 'string' + ? comparableRowValue + : JSON.stringify(comparableRowValue) if (normalizedRowValue === normalizedConflictValue) { // Check if this row already has errors for this column diff --git a/bun.lock b/bun.lock index 848b03f4155..52861a831d2 100644 --- a/bun.lock +++ b/bun.lock @@ -515,6 +515,7 @@ "@sim/utils": "workspace:*", "clsx": "^2.1.1", "tailwind-merge": "3.6.0", + "zod": "4.3.6", }, "devDependencies": { "@radix-ui/react-avatar": "1.1.10", diff --git a/packages/emcn/package.json b/packages/emcn/package.json index 4ad12ce04cc..8b7e5db278a 100644 --- a/packages/emcn/package.json +++ b/packages/emcn/package.json @@ -37,7 +37,8 @@ "dependencies": { "@sim/utils": "workspace:*", "clsx": "^2.1.1", - "tailwind-merge": "3.6.0" + "tailwind-merge": "3.6.0", + "zod": "4.3.6" }, "peerDependencies": { "@radix-ui/react-avatar": "^1.1.10", diff --git a/packages/emcn/src/components/calendar/calendar-interaction.test.tsx b/packages/emcn/src/components/calendar/calendar-interaction.test.tsx new file mode 100644 index 00000000000..bd9fe24939f --- /dev/null +++ b/packages/emcn/src/components/calendar/calendar-interaction.test.tsx @@ -0,0 +1,46 @@ +/** + * @vitest-environment jsdom + */ + +import { act, createElement } from 'react' +import { Calendar } from '@sim/emcn' +import { createRoot } from 'react-dom/client' +import { describe, expect, it, vi } from 'vitest' + +;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + +describe('Calendar precise date edits', () => { + it.each(['07:30:45.123456', '00:00:00.000001', '02:30:00.999999'])( + 'retains %s when selecting a day and Today', + async (time) => { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + const onChange = vi.fn() + try { + await act(async () => + root.render( + createElement(Calendar, { + value: `2026-03-07T${time}`, + showTime: true, + today: '2026-03-09', + onChange, + }) + ) + ) + const buttons = Array.from(container.querySelectorAll('button')) + const nextDay = buttons.find((button) => button.textContent?.trim() === '8') + expect(nextDay).toBeDefined() + await act(async () => nextDay!.click()) + expect(onChange).toHaveBeenLastCalledWith(`2026-03-08T${time}`) + const today = buttons.find((button) => button.textContent?.trim() === 'Today') + expect(today).toBeDefined() + await act(async () => today!.click()) + expect(onChange).toHaveBeenLastCalledWith(`2026-03-09T${time}`) + } finally { + await act(async () => root.unmount()) + container.remove() + } + } + ) +}) diff --git a/packages/emcn/src/components/calendar/calendar.test.ts b/packages/emcn/src/components/calendar/calendar.test.ts index e3056173e45..bcfdc5b4668 100644 --- a/packages/emcn/src/components/calendar/calendar.test.ts +++ b/packages/emcn/src/components/calendar/calendar.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { buildRangeBounds, formatDateRangeLabel, @@ -20,13 +20,42 @@ describe('parseDateTimeValue', () => { expect(parseDateTimeValue('2026-07-06T16:04').time).toBe('16:04') }) + it.each(['14:30:00.000001', '14:30:45.123456', '00:00:00.999999', '14:30:45.123456789'])( + 'retains the full wall time %s for subsequent date selections', + (time) => { + expect(parseDateTimeValue(`2026-09-07T${time}`).time).toBe(time) + } + ) + + it.each([ + ['2026-09-07T07:30:45.123456Z', '07:30:45'], + ['2026-09-07T07:30:45.123456-07:00', '14:30:45'], + ['2026-09-07T07:30:45.123456+05:45', '01:45:45'], + ])('keeps explicit-offset input %s on the instant conversion path', (value, expectedTime) => { + vi.stubEnv('TZ', 'UTC') + try { + expect(parseDateTimeValue(value).time).toBe(expectedTime) + } finally { + vi.unstubAllEnvs() + } + }) + + it('does not reinterpret a literal wall time through a daylight-saving gap', () => { + expect(parseDateTimeValue('2026-03-08T02:30:45.123456').time).toBe('02:30:45.123456') + }) + it('treats a coincidental local midnight as no time for Date instances', () => { expect(parseDateTimeValue(new Date(2026, 6, 6)).time).toBeNull() expect(parseDateTimeValue(new Date(2026, 6, 6, 16, 4, 55)).time).toBe('16:04:55') }) + it('retains early years when reading a literal wall time', () => { + expect(parseDateTimeValue('0001-01-01T12:30:00.123456').date?.getFullYear()).toBe(1) + }) + it('returns nulls for unparseable input', () => { expect(parseDateTimeValue('garbage')).toEqual({ date: null, time: null }) + expect(parseDateTimeValue('2026-99-99T12:30:00.123456')).toEqual({ date: null, time: null }) }) }) diff --git a/packages/emcn/src/components/calendar/calendar.tsx b/packages/emcn/src/components/calendar/calendar.tsx index 28cdadcbd19..9997ec2f7f2 100644 --- a/packages/emcn/src/components/calendar/calendar.tsx +++ b/packages/emcn/src/components/calendar/calendar.tsx @@ -1,6 +1,7 @@ 'use client' import { useMemo, useState } from 'react' +import { z } from 'zod' import { ChevronLeft, ChevronRight } from '../../icons' import { cn } from '../../lib/cn' import { Chip, chipVariants } from '../chip/chip' @@ -27,6 +28,7 @@ const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const const DEFAULT_RANGE_START_TIME = '00:00' const DEFAULT_RANGE_END_TIME = '23:59' +const localDateTimePartsSchema = z.tuple([z.iso.date(), z.iso.time()]) function getDaysInMonth(year: number, month: number): number { return new Date(year, month + 1, 0).getDate() @@ -131,8 +133,9 @@ function timeOfDayFrom(date: Date): string { /** * Parses a date value into its local day plus an optional time-of-day. Bare - * `YYYY-MM-DD` strings are pure days (no time). Datetime strings parse through - * `Date` so an explicit offset (`Z`, `-07:00`) resolves to the **local** day — + * `YYYY-MM-DD` strings are pure days (no time). Offset-free ISO datetimes keep + * their literal clock and fractional precision. Explicit offsets (`Z`, `-07:00`) + * parse through `Date` and resolve to the **local** day — * unlike {@link parseDateValue}'s date-slice fast path, which would read the * UTC day. * @@ -153,6 +156,16 @@ export function parseDateTimeValue(value: string | Date | undefined): { } const parsed = value instanceof Date ? value : new Date(value) if (Number.isNaN(parsed.getTime())) return { date: null, time: null } + if (typeof value === 'string') { + const wallTime = localDateTimePartsSchema.safeParse(value.split('T')) + if (wallTime.success) { + const [, time] = wallTime.data + return { + date: parsed, + time: time.length === 8 && time.endsWith(':00') ? time.slice(0, 5) : time, + } + } + } if (typeof value === 'string' && value.includes('T')) { return { date: parsed, time: timeOfDayFrom(parsed) } } @@ -206,16 +219,18 @@ interface CalendarSingleProps extends CalendarBaseProps { value?: string | Date /** * Called with the picked date in `YYYY-MM-DD` format — or, with `showTime` - * and a set time, the local wall time `YYYY-MM-DDTHH:mm[:ss]`. + * and a set time, the local wall time `YYYY-MM-DDTHH:mm[:ss[.fraction]]`. */ onChange?: (value: string) => void /** * Adds a time-of-day input under the grid. Day picks keep the current time - * (seconds included when the seeded value had them); time edits re-emit on + * (seconds and fractional seconds included when supplied); time edits re-emit on * the selected (or today's) day. Without a time set, day picks emit bare * `YYYY-MM-DD` days. */ showTime?: boolean + /** Label beside the time picker when `showTime` is enabled. Defaults to `Time`. */ + timeLabel?: string /** * Today's calendar day (`YYYY-MM-DD`) in the caller's effective timezone; * drives the Today button and today ring. Defaults to the runtime's local @@ -350,6 +365,7 @@ function SingleCalendarView({ value, onChange, showTime = false, + timeLabel = 'Time', today: todayValue, className, }: CalendarSingleProps) { @@ -424,7 +440,7 @@ function SingleCalendarView({ {showTime && (
- Time + {timeLabel}
)} From b49d28fb5be5b954632515607682c5b39785435f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 14 Sep 2026 15:58:20 -0700 Subject: [PATCH 05/17] fix(chat): render deployed file outputs inline (#7826) * fix(chat): render deployed file outputs inline * fix(chat): reconcile structured output delivery --- .../(interfaces)/chat/[identifier]/chat.tsx | 3 +- .../message/components/file-download.test.tsx | 69 +++++++ .../message/components/file-download.tsx | 53 ++++-- .../chat/components/message/message.test.tsx | 35 ++++ .../chat/components/message/message.tsx | 37 ++-- .../chat/hooks/use-chat-streaming.test.tsx | 132 ++++++++++++- .../chat/hooks/use-chat-streaming.ts | 156 +++++++-------- .../streaming/agent-stream-protocol.test.ts | 30 +++ .../streaming/agent-stream-protocol.ts | 31 ++- .../lib/workflows/streaming/streaming.test.ts | 180 ++++++++++++++++++ apps/sim/lib/workflows/streaming/streaming.ts | 46 ++++- 11 files changed, 642 insertions(+), 130 deletions(-) create mode 100644 apps/sim/app/(interfaces)/chat/components/message/components/file-download.test.tsx diff --git a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx index f066279140d..beadfdc58e9 100644 --- a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx +++ b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx @@ -6,6 +6,7 @@ import { generateId } from '@sim/utils/id' import { AGENT_STREAM_PROTOCOL_HEADER, AGENT_STREAM_PROTOCOL_V1, + CHAT_OUTPUT_PROTOCOL_V1, } from '@/lib/workflows/streaming/agent-stream-protocol' import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar' import { @@ -236,7 +237,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', - [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1, + [AGENT_STREAM_PROTOCOL_HEADER]: `${AGENT_STREAM_PROTOCOL_V1}, ${CHAT_OUTPUT_PROTOCOL_V1}`, }, body: JSON.stringify(payload), credentials: 'same-origin', diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.test.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.test.tsx new file mode 100644 index 00000000000..b386babcad2 --- /dev/null +++ b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.test.tsx @@ -0,0 +1,69 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ChatFileDownload } from '@/app/(interfaces)/chat/components/message/components/file-download' +import type { ChatFile } from '@/app/(interfaces)/chat/components/message/message' + +const imageFile: ChatFile = { + id: 'file-image', + name: 'generated.png', + key: 'execution/generated.png', + url: 'https://files.example.com/generated.png', + size: 3, + type: 'image/png', + base64: 'YWJj', +} + +const mounts: Array<() => void> = [] + +function renderFile(file: ChatFile): HTMLDivElement { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + act(() => root.render()) + mounts.push(() => act(() => root.unmount())) + return container +} + +afterEach(() => { + while (mounts.length) mounts.pop()?.() + vi.restoreAllMocks() +}) + +describe('ChatFileDownload', () => { + it('previews returned image bytes inline without requiring a workspace session', () => { + const container = renderFile(imageFile) + const image = container.querySelector('img') + expect(image?.getAttribute('src')).toBe('data:image/png;base64,YWJj') + expect(image?.alt).toBe('generated.png') + expect(container.querySelector('button')?.textContent).toContain('generated.png') + }) + + it('uses the file URL when inline bytes are unavailable', () => { + const container = renderFile({ ...imageFile, base64: undefined }) + expect(container.querySelector('img')?.getAttribute('src')).toBe(imageFile.url) + }) + + it('uses the canonical serve route for unsafe file URLs', () => { + const container = renderFile({ ...imageFile, base64: undefined, url: 'javascript:alert(1)' }) + expect(container.querySelector('img')?.getAttribute('src')).toBe( + '/api/files/serve/execution%2Fgenerated.png?context=execution' + ) + }) + + it('keeps a download available when an image preview fails', () => { + const container = renderFile(imageFile) + act(() => container.querySelector('img')!.dispatchEvent(new Event('error'))) + expect(container.querySelector('img')).toBeNull() + expect(container.querySelector('button')?.textContent).toContain('generated.png') + }) + + it('renders documents as downloads without an image preview', () => { + const container = renderFile({ ...imageFile, name: 'report.pdf', type: 'application/pdf' }) + expect(container.querySelector('img')).toBeNull() + expect(container.querySelector('button')?.textContent).toContain('report.pdf') + }) +}) diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx index a043bd433df..f9005b8b7e5 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx @@ -51,6 +51,8 @@ function isImageFile(mimeType: string): boolean { } function getFileUrl(file: ChatFile): string { + if (file.base64) return `data:${file.type};base64,${file.base64}` + if (isSafeHttpUrl(file.url)) return file.url return `/api/files/serve/${encodeURIComponent(file.key)}?context=${file.context || 'execution'}` } @@ -76,6 +78,8 @@ async function triggerDownload(url: string, filename: string): Promise { export function ChatFileDownload({ file }: ChatFileDownloadProps) { const [isDownloading, setIsDownloading] = useState(false) + const [failedPreviewUrl, setFailedPreviewUrl] = useState(null) + const fileUrl = getFileUrl(file) const handleDownload = async () => { if (isDownloading) return @@ -109,25 +113,36 @@ export function ChatFileDownload({ file }: ChatFileDownloadProps) { } return ( - +
+ {isImageFile(file.type) && failedPreviewUrl !== fileUrl && ( + {file.name} setFailedPreviewUrl(fileUrl)} + /> + )} + +
) } diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx index 43daf5cd018..1726c423397 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx @@ -87,6 +87,41 @@ describe('ClientChatMessage thinking chrome (Step 6)', () => { } }) + it('renders no message row or copy action for empty assistant output', () => { + const { container, unmount } = renderMessage({ + id: 'empty-output', + type: 'assistant', + content: '', + files: [], + timestamp: new Date(), + }) + mounts.push(unmount) + expect(container.innerHTML).toBe('') + }) + + it('does not show a copy action for a file-only response', () => { + const { container, unmount } = renderMessage({ + id: 'file-output', + type: 'assistant', + content: '', + files: [ + { + id: 'file-1', + name: 'image.png', + url: '/image.png', + key: 'image.png', + size: 3, + type: 'image/png', + }, + ], + timestamp: new Date(), + }) + mounts.push(unmount) + expect(container.querySelector('[data-message-id]')).not.toBeNull() + expect(container.querySelector('[data-testid="answer"]')).toBeNull() + expect(container.textContent).not.toContain('Copy to clipboard') + }) + it('does not show thinking chrome when thinking is absent or empty', () => { const without = renderMessage({ id: '1', diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.tsx index ea2c6d4ab1a..8b4cd29647f 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.tsx @@ -30,6 +30,7 @@ export interface ChatFile { size: number type: string context?: string + base64?: string } /** Chat surface tool chip — the shared lifecycle chip plus its block id. */ @@ -100,11 +101,13 @@ function openAttachmentPreview(name: string, dataUrl: string): void { setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000) } +interface ClientChatMessageProps { + message: ChatMessage +} + export const ClientChatMessage = memo(function ClientChatMessage({ message, -}: { - message: ChatMessage -}) { +}: ClientChatMessageProps) { const [isCopied, setIsCopied] = useState(false) const isJsonObject = typeof message.content === 'object' && message.content !== null @@ -113,6 +116,12 @@ export const ClientChatMessage = memo(function ClientChatMessage({ const cleanTextContent = message.content const hasThinking = typeof message.thinking === 'string' && message.thinking.length > 0 const hasToolCalls = Array.isArray(message.toolCalls) && message.toolCalls.length > 0 + const hasContent = isJsonObject || Boolean((message.content as string).trim()) + const hasFiles = Boolean(message.files?.length) + + if (message.type === 'assistant' && !hasContent && !hasFiles && !hasThinking && !hasToolCalls) { + return null + } const content = message.type === 'user' ? ( @@ -238,15 +247,17 @@ export const ClientChatMessage = memo(function ClientChatMessage({ isStreaming={message.isToolStreaming} /> )} -
- {isJsonObject ? ( -
-                    {JSON.stringify(cleanTextContent, null, 2)}
-                  
- ) : ( - - )} -
+ {hasContent && ( +
+ {isJsonObject ? ( +
+                      {JSON.stringify(cleanTextContent, null, 2)}
+                    
+ ) : ( + + )} +
+ )}
{message.files && message.files.length > 0 && (
@@ -257,7 +268,7 @@ export const ClientChatMessage = memo(function ClientChatMessage({ )} {message.type === 'assistant' && !isJsonObject && !message.isInitialMessage && (
- {!message.isStreaming && ( + {!message.isStreaming && hasContent && ( - - -
-
-
- - +
diff --git a/apps/desktop/static/server.html b/apps/desktop/static/server.html index 44c50e546c2..e096b082283 100644 --- a/apps/desktop/static/server.html +++ b/apps/desktop/static/server.html @@ -2,272 +2,16 @@ + Sim - Server - + + -
-
-

Sim server

-

- Point this app at your own Sim deployment. Self-hosted servers must use HTTPS; localhost may - use HTTP. -

- - -
-
- - -
-
- +
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 77d5b0963ea..3f6edb1844f 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "@sim/tsconfig/base.json", "compilerOptions": { - "lib": ["ES2022"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "paths": { "@/*": ["./src/*"] - } + }, + "jsx": "react-jsx" }, "include": ["src/**/*", "scripts/**/*", "e2e/**/*", "playwright.config.ts", "vitest.config.ts"], "exclude": ["node_modules", "dist", "release"] diff --git a/apps/desktop/turbo.json b/apps/desktop/turbo.json new file mode 100644 index 00000000000..975eab257e2 --- /dev/null +++ b/apps/desktop/turbo.json @@ -0,0 +1,13 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/apps/sim/app/_styles/**", + "$TURBO_ROOT$/apps/sim/postcss.config.mjs", + "$TURBO_ROOT$/apps/sim/lib/postcss/**" + ] + } + } +} diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts index 9718634dfc7..274f7646a01 100644 --- a/apps/desktop/vitest.config.ts +++ b/apps/desktop/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ test: { environment: 'node', globals: true, + setupFiles: ['src/test/setup.ts'], include: ['src/**/*.test.ts'], exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'], pool: 'threads', diff --git a/apps/sim/app/oauth-error/page.tsx b/apps/sim/app/oauth-error/page.tsx index 3818f2eeae9..6e06358de77 100644 --- a/apps/sim/app/oauth-error/page.tsx +++ b/apps/sim/app/oauth-error/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar' +import { DesktopHandoffShell } from '@/app/desktop/components/desktop-handoff-shell' export const metadata: Metadata = { title: 'Sign-in couldn’t be completed', @@ -55,15 +55,9 @@ export default async function OAuthErrorPage({ searchParams }: OAuthErrorPagePro const code = typeof params.error === 'string' ? params.error : undefined return ( -
- -
-

Couldn’t complete that

-

{messageForError(code)}

-

- You can close this tab and try again from Sim. -

-
-
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx index b6853088828..199aa2b8be9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' import type { BrowserPageIssue } from '@sim/browser-protocol' -import { Button } from '@sim/emcn' +import { Chip } from '@sim/emcn' import { CircleAlert, Globe, RefreshCw } from '@sim/emcn/icons' interface BrowserPageIssueProps { @@ -161,10 +161,11 @@ export function BrowserPageIssueView({ issue, onReload, focusRecovery }: Browser ))}

{copy.code}

- +
+ + Reload + +
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-theme-notice.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-theme-notice.tsx index 050f3b76228..b1c53816c34 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-theme-notice.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-theme-notice.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { Banner, Button } from '@sim/emcn' +import { Banner, Chip } from '@sim/emcn' import { X } from '@sim/emcn/icons' import { sendBrowserPanelAction } from '@/lib/browser-agent/transport' @@ -22,25 +22,8 @@ export function BrowserThemeNotice({ scopeId }: BrowserThemeNoticeProps) { Some sites apply theme changes after a reload.

- - + sendBrowserPanelAction('reload', {}, scopeId)}>Reload page + setVisible(false)} />
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx index 3b058443c8b..3ce52bacb91 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx @@ -20,7 +20,7 @@ import { type IBufferRange, Terminal } from '@xterm/xterm' import { useTheme } from 'next-themes' import { useContextMenu } from '@/hooks/use-context-menu' import '@xterm/xterm/css/xterm.css' -import { describeRunningCommand, type TerminalTabsState } from '@sim/terminal-protocol' +import type { TerminalTabsState } from '@sim/terminal-protocol' import { getDesktopBridge } from '@/lib/desktop' import { loadDesktopTerminalAppearance, @@ -47,6 +47,7 @@ import { writeToTerminal, } from '@/lib/terminal/transport' import { TerminalContextMenu } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-context-menu' +import { useTerminalCloseConfirmation } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/use-terminal-close-confirmation' import { useDesktopPreferenceMutation } from '@/hooks/use-desktop-preference-mutation' import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store' import type { ChatContext, TerminalTextSelection } from '@/stores/panel' @@ -234,6 +235,7 @@ const TerminalView = memo(function TerminalView({ const hostRef = useRef(null) const terminalRef = useRef(null) const fitRef = useRef(null) + const { confirmTerminalClose, confirmationDialog } = useTerminalCloseConfirmation(scopeId) const [currentZoom, setCurrentZoom] = useState(defaultZoom) // Being the selected tab is not enough to be on screen: the whole panel is // hidden whenever another resource is open. @@ -590,20 +592,12 @@ const TerminalView = memo(function TerminalView({ }) }, [scopeId]) - // Scoped to the terminal that was right-clicked, not the active one. - const closeThisTerminal = useCallback(() => { - if ( - running && - !window.confirm( - `${describeRunningCommand(running)} is still running. Close this terminal and stop it?` - ) - ) { - return - } + async function closeThisTerminal() { + if (!(await confirmTerminalClose([terminalId]))) return void closeTerminal(terminalId, scopeId).catch(() => { toast.error('Could not close that terminal. Please try again.') }) - }, [running, terminalId, scopeId]) + } // An inactive tab is `display: none`, not merely invisible. xterm watches its // element with an IntersectionObserver and pauses rendering once it stops @@ -613,6 +607,7 @@ const TerminalView = memo(function TerminalView({ // xterm re-measures and does a full refresh when the element comes back. return ( <> + {confirmationDialog}
void)[] = [] + +function renderHook(useHook: () => ReturnType) { + let value: ReturnType | undefined + function Harness() { + value = useHook() + return null + } + const container = document.createElement('div') + const root = createRoot(container) + let mounted = true + const unmount = () => { + if (!mounted) return + mounted = false + act(() => root.unmount()) + } + cleanups.push(unmount) + act(() => root.render()) + return { + result: { + get current() { + if (!value) throw new Error('Hook was not rendered') + return value + }, + }, + rerender: () => act(() => root.render()), + unmount, + } +} + +afterEach(() => { + for (const cleanup of cleanups.splice(0)) cleanup() +}) + +const { getState } = vi.hoisted(() => ({ getState: vi.fn() })) +vi.mock('@/stores/copilot-terminal/store', () => ({ useCopilotTerminalStore: { getState } })) +vi.mock('@sim/emcn', () => ({ ChipConfirmModal: vi.fn(), toast: { warning: vi.fn() } })) + +function setRunning(running: string | null) { + getState.mockReturnValue({ + sessions: { scope: { tabs: { tabs: [{ terminalId: 'terminal', running }] } } }, + }) +} + +beforeEach(() => { + setRunning('sleep 1') +}) + +describe('useTerminalCloseConfirmation', () => { + it('waits for approval and rejects duplicate confirmation requests', async () => { + const { result } = renderHook(() => useTerminalCloseConfirmation('scope')) + let decision: Promise | undefined + act(() => { + decision = result.current.confirmTerminalClose(['terminal']) + }) + await expect(result.current.confirmTerminalClose(['terminal'])).resolves.toBe(false) + act(() => { + result.current.confirmationDialog?.props.confirm.onClick() + }) + await expect(decision).resolves.toBe(true) + expect(result.current.confirmationDialog).toBeNull() + }) + + it('refuses to close when the running command changed while the dialog was open', async () => { + const { result } = renderHook(() => useTerminalCloseConfirmation('scope')) + let decision: Promise | undefined + act(() => { + decision = result.current.confirmTerminalClose(['terminal']) + }) + setRunning('build') + act(() => { + result.current.confirmationDialog?.props.confirm.onClick() + }) + await expect(decision).resolves.toBe(false) + }) + + it('cancels pending confirmation when the caller unmounts', async () => { + const { result, unmount } = renderHook(() => useTerminalCloseConfirmation('scope')) + let decision: Promise | undefined + act(() => { + decision = result.current.confirmTerminalClose(['terminal']) + }) + unmount() + await expect(decision).resolves.toBe(false) + }) + + it('does not resurrect a cancelled dialog when returning to its scope', async () => { + let scopeId = 'scope' + const { result, rerender } = renderHook(() => useTerminalCloseConfirmation(scopeId)) + let decision: Promise | undefined + act(() => { + decision = result.current.confirmTerminalClose(['terminal']) + }) + scopeId = 'another-scope' + rerender() + await expect(decision).resolves.toBe(false) + expect(result.current.confirmationDialog).toBeNull() + scopeId = 'scope' + rerender() + expect(result.current.confirmationDialog).toBeNull() + act(() => { + decision = result.current.confirmTerminalClose(['terminal']) + }) + act(() => { + result.current.confirmationDialog?.props.confirm.onClick() + }) + await expect(decision).resolves.toBe(true) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/use-terminal-close-confirmation.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/use-terminal-close-confirmation.tsx new file mode 100644 index 00000000000..aee7686ceb3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/use-terminal-close-confirmation.tsx @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { ChipConfirmModal, toast } from '@sim/emcn' +import { describeRunningCommand } from '@sim/terminal-protocol' +import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store' + +interface TerminalCloseRequest { + scopeId: string + targets: { terminalId: string; running: string | null }[] + resolve: (confirmed: boolean) => void +} + +/** Confirms the captured terminals and rechecks their commands before closing them. */ +export function useTerminalCloseConfirmation(scopeId: string) { + const pendingRef = useRef(null) + const [request, setRequest] = useState(null) + const [previousScopeId, setPreviousScopeId] = useState(scopeId) + + if (previousScopeId !== scopeId) { + setPreviousScopeId(scopeId) + setRequest(null) + } + + useEffect(() => { + return () => { + pendingRef.current?.resolve(false) + pendingRef.current = null + } + }, [scopeId]) + + const confirmTerminalClose = useCallback( + (terminalIds: string[]): Promise => { + if (pendingRef.current) return Promise.resolve(false) + const tabs = useCopilotTerminalStore.getState().sessions[scopeId]?.tabs.tabs ?? [] + const targets = tabs + .filter((tab) => terminalIds.includes(tab.terminalId)) + .map(({ terminalId, running }) => ({ terminalId, running })) + if (!targets.some((target) => target.running)) return Promise.resolve(true) + return new Promise((resolve) => { + const next = { scopeId, targets, resolve } + pendingRef.current = next + setRequest(next) + }) + }, + [scopeId] + ) + + function settle(confirmed: boolean) { + const pending = pendingRef.current + if (!pending) return + if (confirmed) { + const tabs = useCopilotTerminalStore.getState().sessions[pending.scopeId]?.tabs.tabs ?? [] + confirmed = pending.targets.every((target) => { + const current = tabs.find((tab) => tab.terminalId === target.terminalId) + return current && (!current.running || current.running === target.running) + }) + if (!confirmed) toast.warning('A terminal changed. Review it before closing.') + } + pendingRef.current = null + setRequest(null) + pending.resolve(confirmed) + } + + const running = + request?.targets.flatMap((target) => (target.running ? [target.running] : [])) ?? [] + const confirmationDialog = + request?.scopeId === scopeId ? ( + { + if (!open) settle(false) + }} + title={request.targets.length === 1 ? 'Close terminal?' : 'Close terminals?'} + text={ + running.length === 1 + ? `${describeRunningCommand(running[0])} is still running. Closing the terminal will stop it.` + : `${running.length} selected terminals have a running process. Closing these terminals will stop them.` + } + confirm={{ + label: request.targets.length === 1 ? 'Close terminal' : 'Close terminals', + variant: 'destructive', + onClick: () => settle(true), + }} + /> + ) : null + + return { confirmTerminalClose, confirmationDialog } +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index 9f1d18d94ad..1e17cf742fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -21,7 +21,7 @@ import { toast, } from '@sim/emcn' import { Columns3, Eye, Pencil } from '@sim/emcn/icons' -import { describeRunningCommand, type TerminalTabState } from '@sim/terminal-protocol' +import type { TerminalTabState } from '@sim/terminal-protocol' import { browserTabTitle } from '@/lib/browser-agent/tab-label' import { openBrowserTab, @@ -37,6 +37,7 @@ import { closeTerminal, openTerminal, reorderTerminal } from '@/lib/terminal/tra import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context' import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' +import { useTerminalCloseConfirmation } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/use-terminal-close-confirmation' import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' import { RESOURCE_HEADER_CLASSES, @@ -77,24 +78,6 @@ const ADD_RESOURCE_EXCLUDED_TYPES: readonly MothershipResourceType[] = [ const EMPTY_TERMINAL_TABS: TerminalTabState[] = [] -/** Closing a shell mid-command stops that command, so the user confirms first. */ -function confirmClosingRunningTerminals( - targets: readonly MothershipResource[], - terminalTabs: readonly TerminalTabState[] -): boolean { - const running = targets.flatMap((resource) => { - if (resource.type !== 'terminal') return [] - const tab = terminalTabs.find((entry) => terminalResourceId(entry.terminalId) === resource.id) - return tab?.running ? [tab.running] : [] - }) - if (running.length === 0) return true - return window.confirm( - running.length === 1 - ? `${describeRunningCommand(running[0])} is still running. Close this terminal and stop it?` - : `${running.length} selected terminals have a running process. Close them anyway?` - ) -} - /** * Returns the id of the nearest resource to `idx` that is in `filter` * (or any resource if `filter` is null). Returns undefined if nothing qualifies. @@ -242,6 +225,7 @@ export function ResourceTabs({ const removeResource = useRemoveChatResource(chatId) const reorderResources = useReorderChatResources(chatId) + const { confirmTerminalClose, confirmationDialog } = useTerminalCloseConfirmation(desktopScopeId) const [selectedIds, setSelectedIds] = useState>(new Set()) const anchorIdRef = useRef(null) const prevChatIdRef = useRef(chatId) @@ -407,13 +391,16 @@ export function ResourceTabs({ ) const handleClose = useCallback( - (id: string) => { + async (id: string) => { const index = resources.findIndex((r) => r.id === id) const resource = resources[index] if (!resource) return const isMulti = selectedIds.has(resource.id) && selectedIds.size > 1 const targets = isMulti ? resources.filter((r) => selectedIds.has(r.id)) : [resource] - if (!confirmClosingRunningTerminals(targets, terminalTabs)) return + const terminalIds = targets + .filter((target) => target.type === 'terminal') + .map((target) => terminalIdFromResourceId(target.id)) + if (!(await confirmTerminalClose(terminalIds))) return // Closing the shown tab moves to its neighbour, right then left, so the // strip does not fall back to its last tab and jump. For a desktop tab // this is also the neighbour the desktop app itself picks. @@ -469,7 +456,7 @@ export function ResourceTabs({ resources, selectResource, selectedIds, - terminalTabs, + confirmTerminalClose, ] ) @@ -574,38 +561,44 @@ export function ResourceTabs({ ) : null return ( - - -
- } - // A bare fragment is always truthy, so the empty case has to be `null` or - // the strip renders an empty trailing cluster. - endActions={ - actions || previewToggle ? ( - <> - {actions} - {previewToggle} - - ) : null - } - /> + <> + {confirmationDialog} + + + + } + // A bare fragment is always truthy, so the empty case has to be `null` or + // the strip renders an empty trailing cluster. + endActions={ + actions || previewToggle ? ( + <> + {actions} + {previewToggle} + + ) : null + } + /> + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/not-found.tsx b/apps/sim/app/workspace/[workspaceId]/not-found.tsx index 6a29b6c5a26..58cc12614db 100644 --- a/apps/sim/app/workspace/[workspaceId]/not-found.tsx +++ b/apps/sim/app/workspace/[workspaceId]/not-found.tsx @@ -1,8 +1,7 @@ 'use client' -import { Button, buttonVariants } from '@sim/emcn' +import { Chip, ChipLink } from '@sim/emcn' import { ArrowLeft, Compass, Home } from '@sim/emcn/icons' -import Link from 'next/link' import { useParams, useRouter } from 'next/navigation' import { ErrorShell } from '@/app/workspace/[workspaceId]/components' @@ -17,14 +16,12 @@ export default function WorkspaceNotFound() { description="The page you're looking for doesn't exist or has been moved. Head back to your workspace to keep building." icon={} > - - - + + Return home - + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/error.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/error.tsx index d17ae78e43d..78d2ed68ff8 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/error.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/error.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button } from '@sim/emcn' +import { Chip } from '@sim/emcn' import { ArrowLeft } from '@sim/emcn/icons' import { useParams, useRouter } from 'next/navigation' import { type ErrorBoundaryProps, ErrorState } from '@/app/workspace/[workspaceId]/components' @@ -17,14 +17,9 @@ export default function TableError({ error, reset }: ErrorBoundaryProps) { description='Something went wrong while loading this table. The table may have been deleted or you may not have permission to view it.' loggerName='TableError' > - + ) } diff --git a/bun.lock b/bun.lock index 52861a831d2..d3a4a9f48fd 100644 --- a/bun.lock +++ b/bun.lock @@ -61,13 +61,20 @@ "devDependencies": { "@electron/fuses": "1.8.0", "@playwright/test": "1.61.1", + "@sim/emcn": "workspace:*", "@sim/tsconfig": "workspace:*", "@types/micromatch": "4.0.10", "@types/node": "24.2.1", + "@types/react": "^19", + "@types/react-dom": "^19", "electron": "43.5.0", "electron-builder": "26.15.3", "esbuild": "0.28.1", "jsdom": "^26.0.0", + "postcss": "^8", + "postcss-load-config": "6.0.1", + "react": "19.2.4", + "react-dom": "19.2.4", "typescript": "^7.0.2", "vitest": "^4.1.0", }, diff --git a/packages/emcn/src/components/chip-modal/chip-modal.tsx b/packages/emcn/src/components/chip-modal/chip-modal.tsx index 87f1b872abf..8ad12f7c3dc 100644 --- a/packages/emcn/src/components/chip-modal/chip-modal.tsx +++ b/packages/emcn/src/components/chip-modal/chip-modal.tsx @@ -219,6 +219,35 @@ export interface ChipModalProps { children?: React.ReactNode } +/** + * Shared modal chrome and Enter-key policy. Native windows can host this + * surface directly when the operating system owns the dialog lifecycle. + * Web dialogs use it through {@link ChipModal}. + */ +export const ChipModalSurface = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, onKeyDown, ...props }, ref) => ( +
{ + onKeyDown?.(event) + handleChipModalEnter(event) + }} + {...props} + > +
+ {children} +
+
+)) + +ChipModalSurface.displayName = 'ChipModalSurface' + /** * Root component. Wraps the Radix dialog and renders the panel chrome. * Subcomponents (`ChipModalHeader`, `ChipModalBody`, `ChipModalField`, @@ -245,19 +274,9 @@ function ChipModal({ size={size} dismissDisabled={dismissDisabled} onOpenAutoFocus={focusChipModalDefaultAction} - onKeyDown={handleChipModalEnter} aria-describedby={ariaDescribedBy} > -
-
- {children} -
-
+ {children} ) @@ -409,6 +428,7 @@ const ChipModalBody = React.forwardRef( ({ className, fullBleed = false, ...props }, ref) => (
- Cancel + {cancelLabel} )} {primaryAdjacentAction ? renderFooterSlotAction(primaryAdjacentAction) : null} @@ -1577,8 +1596,7 @@ export interface ChipConfirmModalProps { defaultAction?: ChipConfirmDefaultAction /** * Label for the dismiss button. In a confirmation the dismiss button is a - * named decision, so this is honest API (unlike a form footer's structural - * Cancel). Defaults to `'Cancel'`; pass `'Keep editing'` for unsaved-changes. + * named decision. Defaults to `'Cancel'`; pass `'Keep editing'` for unsaved-changes. * @default 'Cancel' */ dismissLabel?: string diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index 20e11b11f4c..ecfcb816719 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -89,6 +89,7 @@ export { type ChipModalPromptBodyProps, type ChipModalProps, ChipModalSeparator, + ChipModalSurface, type ChipModalTab, ChipModalTabs, type ChipModalTabsProps, From 8a4a0e188d7c2f23565c97f67c4d5c2a4cdfe416 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 14 Sep 2026 16:26:30 -0700 Subject: [PATCH 07/17] improvement(search): speed up organization integration and source queries (#7830) --- apps/sim/app/api/files/authorization.test.ts | 9 +- .../sim-search/sources/route.test.ts | 2 +- .../integrations/integrations.test.tsx | 2 +- .../[connectorType]/provider-detail.test.tsx | 3 +- .../kb/search-source-progress.test.tsx | 18 +- .../lib/api/contracts/knowledge/connectors.ts | 6 +- .../knowledge/personal-integrations.ts | 1 - .../load-search-integrations.test.ts | 1 - .../github-member.integration.ts | 14 +- ...rganization-search-overview.integration.ts | 2 +- .../lib/knowledge/access/confluence-site.ts | 13 +- .../knowledge/access/github-installation.ts | 16 +- apps/sim/lib/knowledge/access/live-sources.ts | 41 + apps/sim/lib/knowledge/access/scope.test.ts | 61 +- apps/sim/lib/knowledge/access/scope.ts | 35 +- apps/sim/lib/knowledge/access/types.ts | 11 +- .../knowledge/application/connectors.test.ts | 8 +- .../lib/knowledge/application/connectors.ts | 66 +- .../knowledge/application/documents.test.ts | 3 +- .../personal-search-integrations.test.ts | 15 +- .../personal-search-integrations.ts | 14 +- .../application/search-source-overview.ts | 63 +- .../application/search-sources.test.ts | 95 +- .../knowledge/application/search-sources.ts | 79 +- .../lib/knowledge/documents/service.test.ts | 1 + apps/sim/lib/knowledge/read-access.test.ts | 45 +- apps/sim/lib/knowledge/read-access.ts | 43 +- apps/sim/lib/knowledge/service.test.ts | 25 +- apps/sim/lib/sim-search/source-status.ts | 3 +- ...5_document_connector_processing_status.sql | 6 + .../db/migrations/meta/0345_snapshot.json | 26613 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 10 + 33 files changed, 27108 insertions(+), 223 deletions(-) create mode 100644 apps/sim/lib/knowledge/access/live-sources.ts create mode 100644 packages/db/migrations/0345_document_connector_processing_status.sql create mode 100644 packages/db/migrations/meta/0345_snapshot.json diff --git a/apps/sim/app/api/files/authorization.test.ts b/apps/sim/app/api/files/authorization.test.ts index b449eab962f..a8738342a8d 100644 --- a/apps/sim/app/api/files/authorization.test.ts +++ b/apps/sim/app/api/files/authorization.test.ts @@ -436,13 +436,15 @@ describe('KB file live source authorization', () => { async (allowed) => { const scope = { kind: 'user' as const, userId: USER_ID, tokens: ['reader-token'] } const getForConnectors = vi.fn().mockResolvedValue(scope) + const liveSources = { type: 'live-sources' } const access: KnowledgeAccessProvider = { get: async () => scope, getForConnectors, getForDocuments: async () => scope, + liveSourceConnectorCondition: async () => liveSources as never, } queueTableRows(schemaMock.document, []) - queueTableRows(schemaMock.document, [{ connectorId: 'confluence-source' }]) + queueTableRows(schemaMock.knowledgeConnector, [{ connectorId: 'confluence-source' }]) queueTableRows(schemaMock.document, allowed ? [{ id: 'doc-1' }] : []) await expect( verifyFileAccess(CLOUD_KEY, USER_ID, undefined, 'knowledge-base', false, { @@ -450,7 +452,12 @@ describe('KB file live source authorization', () => { }) ).resolves.toBe(allowed) expect(getForConnectors).toHaveBeenCalledExactlyOnceWith(['confluence-source'], undefined) + const discovery = dbChainMockFns.where.mock.calls.filter(([condition]) => + hasMockCondition(condition, (node) => node === liveSources) + ) + expect(discovery).toHaveLength(1) for (const [condition] of dbChainMockFns.where.mock.calls) { + if (discovery.some(([live]) => live === condition)) continue expect( hasMockCondition( condition, diff --git a/apps/sim/app/api/knowledge/sim-search/sources/route.test.ts b/apps/sim/app/api/knowledge/sim-search/sources/route.test.ts index 44ee57d8279..8456c037206 100644 --- a/apps/sim/app/api/knowledge/sim-search/sources/route.test.ts +++ b/apps/sim/app/api/knowledge/sim-search/sources/route.test.ts @@ -59,7 +59,7 @@ const source = { isSyncing: false, lastSyncAt: null, hasSyncError: false, - viewerDocumentCount: 0, + hasViewerDocuments: false, viewerFailedDocumentCount: 0, viewerEmailVerified: true, viewerAccounts: [], diff --git a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx index fa22c0f7467..22a26645a65 100644 --- a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx +++ b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx @@ -149,7 +149,7 @@ const memberSource: SearchSourceSummary = { isSyncing: false, lastSyncAt: null, hasSyncError: false, - viewerDocumentCount: 0, + hasViewerDocuments: false, viewerFailedDocumentCount: 0, viewerEmailVerified: true, viewerAccounts: [], diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx index 7d4d860870c..45566763b4c 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx @@ -136,7 +136,7 @@ const source = { lastSyncAt: '2026-09-08T12:00:00.000Z', connectionRequired: true, viewerMembership: 'needs_reauth', - viewerDocumentCount: 0, + hasViewerDocuments: false, } const credentialGroup = { id: 'accounts-one', @@ -429,7 +429,6 @@ describe('organization provider management', () => { ) expect(container.textContent).not.toContain('Connect account') expect(container.textContent).not.toContain('Reconnect') - expect(container.textContent).not.toContain('0 searchable documents') }) it('loads sources for a nonpersonal provider even when an accounts view URL is supplied', async () => { diff --git a/apps/sim/hooks/queries/kb/search-source-progress.test.tsx b/apps/sim/hooks/queries/kb/search-source-progress.test.tsx index 153b7e3907e..62d13f3779b 100644 --- a/apps/sim/hooks/queries/kb/search-source-progress.test.tsx +++ b/apps/sim/hooks/queries/kb/search-source-progress.test.tsx @@ -23,7 +23,7 @@ function Probe() { const sync = useTriggerSync() return (
- {result.data?.[0]?.viewerDocumentCount ?? 0} + {String(result.data?.[0]?.hasViewerDocuments ?? false)}
' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[WARNING]') expect(result).toContain('Do NOT use this form for GitLab access.') }) @@ -242,7 +241,7 @@ describe('preserveConfluenceCallouts', () => { '
' + '

Heads up.

' + '
' - expect(preserveConfluenceCallouts(html)).toContain('[INFO] Heads up.') + expect(confluenceViewToPlainText(html)).toContain('[INFO] Heads up.') }) it.concurrent('labels a built-in note macro', () => { @@ -250,7 +249,7 @@ describe('preserveConfluenceCallouts', () => { '
' + '

See also.

' + '
' - expect(preserveConfluenceCallouts(html)).toContain('[NOTE] See also.') + expect(confluenceViewToPlainText(html)).toContain('[NOTE] See also.') }) it.concurrent('labels a built-in tip macro', () => { @@ -258,7 +257,7 @@ describe('preserveConfluenceCallouts', () => { '
' + '

Pro tip.

' + '
' - expect(preserveConfluenceCallouts(html)).toContain('[TIP] Pro tip.') + expect(confluenceViewToPlainText(html)).toContain('[TIP] Pro tip.') }) it.concurrent('labels a generic custom-colored Panel macro using its header title', () => { @@ -267,7 +266,7 @@ describe('preserveConfluenceCallouts', () => { '
Do NOT use this form for:
' + '

GitLab access requests go to the private channel instead.

' + '
' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[CALLOUT: Do NOT use this form for:]') expect(result).toContain('GitLab access requests go to the private channel instead.') }) @@ -276,7 +275,7 @@ describe('preserveConfluenceCallouts', () => { const html = '
Warning:
' + '

See replacement form.

' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[CALLOUT: Warning:] See replacement form.') }) @@ -286,7 +285,7 @@ describe('preserveConfluenceCallouts', () => { const html = '
Warning: Do not use
' + '

See replacement form.

' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[CALLOUT: Warning: Do not use]') } ) @@ -294,13 +293,13 @@ describe('preserveConfluenceCallouts', () => { it.concurrent('falls back to a bare CALLOUT label when a Panel macro has no header text', () => { const html = '

Untitled panel body.

' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[CALLOUT]') expect(result).toContain('Untitled panel body.') }) it.concurrent( - 'keeps the exclusion marker attached to its content through htmlToPlainText, even across surrounding whitespace collapse', + 'keeps the exclusion marker attached to its content across surrounding whitespace collapse', () => { const html = '

Intro paragraph.

\n\n' + @@ -309,7 +308,7 @@ describe('preserveConfluenceCallouts', () => { '
  • GitLab
' + '\n\n' + '

Trailing paragraph.

' - const plainText = htmlToPlainText(preserveConfluenceCallouts(html)) + const plainText = confluenceViewToPlainText(html) expect(plainText).toContain('[WARNING] Do NOT use this form for: GitLab') expect(plainText).toContain('Intro paragraph.') expect(plainText).toContain('Trailing paragraph.') @@ -325,7 +324,7 @@ describe('preserveConfluenceCallouts', () => { '

Do NOT use this form for:

' + '
  • GitLab
  • ServiceNow
' + '' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).not.toContain('for:GitLab') expect(result).not.toContain('GitLabServiceNow') expect(result).toContain('Do NOT use this form for: GitLab ServiceNow') @@ -339,7 +338,7 @@ describe('preserveConfluenceCallouts', () => { '
' + '

First sentence.

Second sentence.

' + '
' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('First sentence. Second sentence.') expect(result).not.toContain('sentence.Second') } @@ -355,7 +354,7 @@ describe('preserveConfluenceCallouts', () => { '
  • Nested item A
  • Nested item B
' + '
  • Outer item two
  • ' + '' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) // Each nested
  • 's text must appear exactly once, not duplicated by the // outer
  • also being matched and its .text() recursing into it. const occurrences = (result.match(/Nested item A/g) ?? []).length @@ -370,7 +369,7 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '
    Cell text

    quoted text

    after quote
    ' + '
    ' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).not.toContain('quotedtext') expect(result).not.toContain('textafter') expect(result).toContain('Cell text quoted text after quote') @@ -383,7 +382,7 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '

    This is unbelieveable.

    ' + '
    ' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).not.toContain('un believe able') expect(result).toContain('This is unbelieveable.') } @@ -394,7 +393,7 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '

    Do not proceed!

    ' + '
    ' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).not.toContain('proceed !') expect(result).toContain('[WARNING] Do not proceed!') }) @@ -404,7 +403,7 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '

    Do NOT use this form.

    ' + '
    ' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('Do NOT use this form.') }) @@ -416,7 +415,7 @@ describe('preserveConfluenceCallouts', () => { '
    Inner
    ' + '

    inner body

    ' + '' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[CALLOUT: Outer]') expect(result).toContain('[CALLOUT: Inner] inner body') } @@ -430,7 +429,7 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '

    Do not use this.

    ' + '' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).toContain('[WARNING] Do not use this.') } ) @@ -443,7 +442,7 @@ describe('preserveConfluenceCallouts', () => { '
    Inner title
    ' + '

    inner body

    ' + '' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) // The outer panel has no header of its own — it must fall back to a // bare [CALLOUT], not steal "Inner title" from the nested panel. expect(result).toContain('[CALLOUT] [CALLOUT: Inner title] inner body') @@ -455,10 +454,98 @@ describe('preserveConfluenceCallouts', () => { '
    ' + '

    Do NOT use this form for:
    GitLab

    ' + '
    ' - const result = preserveConfluenceCallouts(html) + const result = confluenceViewToPlainText(html) expect(result).not.toContain('for:GitLab') expect(result).toContain('[WARNING] Do NOT use this form for: GitLab') }) + + it.concurrent('drops app macro bootstrap scripts, inline styles, and chart data', () => { + const html = + '

    ' + + 'Colored text

    ' + + '' + + '
    ' + + '
    ' + + '' + + '

    After

    ' + expect(confluenceViewToPlainText(html)).toBe('Colored text After') + }) + + it.concurrent('keeps the word break a dropped script or style occupied', () => { + expect(confluenceViewToPlainText('

    BeforeAfter

    ')).toBe('Before After') + }) + + it.concurrent('treats a page holding only an app macro as having no text', () => { + const html = + '
    ' + + '
    ' + expect(confluenceViewToPlainText(html)).toBe('') + }) + + it.concurrent('reduces an unresolved Jira issue macro to its issue key', () => { + const html = + '

    Tracked in ' + + '' + + '' + + 'ENG-101 - ' + + 'Getting issue details... ' + + 'STATUS' + + ' and ' + + '' + + '' + + ' ENG-102 - ' + + '이슈 세부사항 가져오는 중... ' + + '상태' + + '.

    ' + expect(confluenceViewToPlainText(html)).toBe('Tracked in ENG-101 and ENG-102 .') + }) + + it.concurrent('falls back to the data attribute when the issue key link has no text', () => { + const html = + '' + + 'Getting issue details...' + + 'STATUS' + expect(confluenceViewToPlainText(html)).toBe('ENG-103') + }) + + it.concurrent('keeps the summary and status of a Jira issue macro Confluence resolved', () => { + const html = + '' + + '' + + 'OPS-201 - ' + + 'Rotate the signing key ' + + 'Done' + + '' + expect(confluenceViewToPlainText(html)).toBe('OPS-201 - Rotate the signing key Done') + }) + + it.concurrent('keeps the block break of a Jira issue macro inside a callout', () => { + const html = + '
    ' + + '
    Blocked by' + + '
    ' + + 'ENG-104 - ' + + 'Getting issue details...' + + 'STATUS
    ' + + 'until release
    ' + expect(confluenceViewToPlainText(html)).toBe('[WARNING] Blocked by ENG-104 until release') + }) + + it.concurrent('drops only the placeholder shell of a Jira issues table', () => { + const html = + '

    Release notes

    ' + + '
    ' + + '
    ' + + '
    typekeysummary
    ' + + '
    Loading...
    ' + + '
    ' + + '' + + '
    keysummary
    ENG-104Update the runbook
    ' + + '
    No issues found
    ' + expect(confluenceViewToPlainText(html)).toBe( + 'Release notes key summary ENG-104 Update the runbook No issues found' + ) + }) }) describe('confluence incremental CQL listing', () => { @@ -1055,7 +1142,29 @@ describe('Confluence permission-scoped content', () => { ) expect(document?.content).toContain('CONFIDENTIAL SALARY DATA') - expect(document?.contentHash).toBe('confluence:view-callouts:shared-page:1') + expect(document?.contentHash).toBe('confluence:view-text-v2:shared-page:1') + }) + + it('reports the content type of the endpoint that answered and omits unknown metadata', async () => { + vi.mocked(fetch).mockResolvedValueOnce(new Response('', { status: 404 })) + + const document = await confluenceConnector.getDocument('token', config, 'shared-page', { + cloudId: 'cloud-1', + }) + + expect(vi.mocked(fetch).mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ + '/ex/confluence/cloud-1/wiki/api/v2/pages/shared-page', + '/ex/confluence/cloud-1/wiki/api/v2/blogposts/shared-page', + ]) + expect(document?.metadata).toEqual({ + spaceId: 'space-1', + contentType: 'blogpost', + status: 'current', + version: 1, + labels: [], + lastModified: '', + }) + expect(document?.metadata).not.toHaveProperty('spaceKey') }) it('rejects a missing storage body without falling back to rendered content', async () => { @@ -1118,7 +1227,7 @@ describe('Confluence permission-scoped content', () => { const expectedHash = 'mirrorsSourceAcls' in mode || 'perMemberListing' in mode ? 'confluence:storage-local-body-v2:shared-page:1' - : 'confluence:view-callouts:shared-page:1' + : 'confluence:view-text-v2:shared-page:1' expect(v2.documents[0].contentHash).toBe(expectedHash) expect(cql.documents[0].contentHash).toBe(expectedHash) diff --git a/apps/sim/connectors/confluence/confluence.ts b/apps/sim/connectors/confluence/confluence.ts index 263d50c7db0..f0da25bdb44 100644 --- a/apps/sim/connectors/confluence/confluence.ts +++ b/apps/sim/connectors/confluence/confluence.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { filterUndefined } from '@sim/utils/object' import * as cheerio from 'cheerio' import { AtlassianSiteNotAccessibleError, @@ -172,9 +173,37 @@ function extractBlockJoinedText($: cheerio.CheerioAPI, $el: cheerio.Cheerio return parts.join(' ').trim() } -/** Matches either flavor of panel/macro this function rewrites. */ +/** Matches either flavor of panel/macro {@link rewriteConfluenceCallouts} rewrites. */ const MACRO_SELECTOR = 'div.confluence-information-macro, div.panel' +/** + * Rendered-page elements whose text is never page prose. App macros render as a + * bootstrap `