From 00a83d0289d84f2e7957980e7efd302e220178d6 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 00:24:02 -0700 Subject: [PATCH 1/8] feat(projects): add project identity and lifecycle foundation --- apps/docs/openapi-v2-resources.json | 66 +- apps/sim/app/api/projects/[id]/route.ts | 45 + .../by-workspace/[workspaceId]/route.ts | 18 + apps/sim/app/api/projects/route.ts | 18 + apps/sim/app/api/workflows/route.test.ts | 3 + .../components/group-detail.tsx | 11 + .../components/project-issue-restrictions.tsx | 60 + .../lib/copy/copy-workflows.test.ts | 26 +- .../lib/copy/copy-workflows.ts | 2 + .../workspace-forking/lib/create-fork.test.ts | 16 +- .../ee/workspace-forking/lib/create-fork.ts | 13 +- .../lib/lineage/unlink.test.ts | 23 +- .../workspace-forking/lib/lineage/unlink.ts | 12 +- apps/sim/hooks/queries/projects.ts | 25 + apps/sim/lib/api/contracts/projects.ts | 86 + .../billing/organizations/lock-order.test.ts | 1 + .../lib/billing/organizations/membership.ts | 21 + .../sim/lib/permission-groups/capabilities.ts | 9 + apps/sim/lib/permission-groups/fields.test.ts | 2 + apps/sim/lib/permission-groups/fields.ts | 5 + .../lib/permission-groups/group-manager.ts | 36 +- .../__integration__/foundation.integration.ts | 619 + apps/sim/lib/projects/account-deletion.ts | 83 + .../lib/projects/application/authorization.ts | 128 + apps/sim/lib/projects/application/index.ts | 9 + .../lib/projects/application/operations.ts | 52 + .../sim/lib/projects/application/use-cases.ts | 204 + apps/sim/lib/projects/lifecycle.ts | 66 + apps/sim/lib/projects/membership.ts | 238 + apps/sim/lib/users/account-deletion.ts | 2 + apps/sim/lib/workflows/lifecycle.ts | 216 +- .../orchestration/workflow-lifecycle.test.ts | 5 + .../workflows/persistence/duplicate.test.ts | 3 + .../workflows/persistence/new-workflow-row.ts | 4 +- .../__integration__/fork-sync.integration.ts | 24 + apps/sim/lib/workspaces/active-workspace.ts | 19 + apps/sim/lib/workspaces/admin-move.ts | 3 + apps/sim/lib/workspaces/create.test.ts | 3 + apps/sim/lib/workspaces/create.ts | 8 + apps/sim/lib/workspaces/lifecycle.test.ts | 99 - apps/sim/lib/workspaces/lifecycle.ts | 228 +- .../organization-workspaces.integration.ts | 1 + .../lib/workspaces/organization-workspaces.ts | 3 + docs/plans/project-backfill-runbook.md | 68 + docs/plans/project-entity-foundation.md | 311 + knip.jsonc | 2 + packages/audit/src/types.ts | 3 + .../db/migrations/0393_project_foundation.sql | 25 + .../db/migrations/meta/0393_snapshot.json | 29715 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/package.json | 4 + packages/db/project-backfill.ts | 471 + packages/db/schema.ts | 49 + packages/db/scripts/backfill-projects.ts | 168 + .../scripts/project-backfill.integration.ts | 396 + packages/sim-cli/src/generated/v2-api.ts | 9 + .../src/mocks/schema-tables.generated.ts | 2 + 57 files changed, 33404 insertions(+), 341 deletions(-) create mode 100644 apps/sim/app/api/projects/[id]/route.ts create mode 100644 apps/sim/app/api/projects/by-workspace/[workspaceId]/route.ts create mode 100644 apps/sim/app/api/projects/route.ts create mode 100644 apps/sim/ee/access-control/components/project-issue-restrictions.tsx create mode 100644 apps/sim/hooks/queries/projects.ts create mode 100644 apps/sim/lib/api/contracts/projects.ts create mode 100644 apps/sim/lib/projects/__integration__/foundation.integration.ts create mode 100644 apps/sim/lib/projects/account-deletion.ts create mode 100644 apps/sim/lib/projects/application/authorization.ts create mode 100644 apps/sim/lib/projects/application/index.ts create mode 100644 apps/sim/lib/projects/application/operations.ts create mode 100644 apps/sim/lib/projects/application/use-cases.ts create mode 100644 apps/sim/lib/projects/lifecycle.ts create mode 100644 apps/sim/lib/projects/membership.ts create mode 100644 apps/sim/lib/workspaces/active-workspace.ts delete mode 100644 apps/sim/lib/workspaces/lifecycle.test.ts create mode 100644 docs/plans/project-backfill-runbook.md create mode 100644 docs/plans/project-entity-foundation.md create mode 100644 packages/db/migrations/0393_project_foundation.sql create mode 100644 packages/db/migrations/meta/0393_snapshot.json create mode 100644 packages/db/project-backfill.ts create mode 100644 packages/db/scripts/backfill-projects.ts create mode 100644 packages/db/scripts/project-backfill.integration.ts diff --git a/apps/docs/openapi-v2-resources.json b/apps/docs/openapi-v2-resources.json index e9ec3dde864..0be846c9c8a 100644 --- a/apps/docs/openapi-v2-resources.json +++ b/apps/docs/openapi-v2-resources.json @@ -15996,6 +15996,16 @@ "disableKnowledgeBaseExport": { "type": "boolean", "description": "Prevent downloading a whole knowledge base as an archive." + }, + "deniedPartialAccessProjectIssues": { + "default": [], + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": "Issues in the listed Projects are unavailable to teammates without access to every active environment." } }, "required": [ @@ -16040,7 +16050,8 @@ "disableToolAutoApproval", "hideSandboxesTab", "disableOAuthAppAccess", - "disableKnowledgeBaseExport" + "disableKnowledgeBaseExport", + "deniedPartialAccessProjectIssues" ], "additionalProperties": false, "description": "Resolved restrictions. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none." @@ -16170,7 +16181,8 @@ "disableToolAutoApproval": false, "hideSandboxesTab": false, "disableOAuthAppAccess": false, - "disableKnowledgeBaseExport": false + "disableKnowledgeBaseExport": false, + "deniedPartialAccessProjectIssues": [] }, "isDefault": false, "membershipMode": "inherit", @@ -16245,7 +16257,8 @@ "disableToolAutoApproval": false, "hideSandboxesTab": false, "disableOAuthAppAccess": false, - "disableKnowledgeBaseExport": false + "disableKnowledgeBaseExport": false, + "deniedPartialAccessProjectIssues": [] }, "isDefault": false, "membershipMode": "inherit", @@ -16499,6 +16512,15 @@ "disableKnowledgeBaseExport": { "type": "boolean", "description": "Prevent downloading a whole knowledge base as an archive." + }, + "deniedPartialAccessProjectIssues": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": "Issues in the listed Projects are unavailable to teammates without access to every active environment." } }, "additionalProperties": false, @@ -16590,7 +16612,8 @@ "disableToolAutoApproval": false, "hideSandboxesTab": false, "disableOAuthAppAccess": false, - "disableKnowledgeBaseExport": false + "disableKnowledgeBaseExport": false, + "deniedPartialAccessProjectIssues": [] }, "isDefault": false, "membershipMode": "inherit", @@ -16663,7 +16686,8 @@ "disableToolAutoApproval": false, "hideSandboxesTab": false, "disableOAuthAppAccess": false, - "disableKnowledgeBaseExport": false + "disableKnowledgeBaseExport": false, + "deniedPartialAccessProjectIssues": [] }, "isDefault": false, "membershipMode": "inherit", @@ -16924,6 +16948,15 @@ "disableKnowledgeBaseExport": { "type": "boolean", "description": "Prevent downloading a whole knowledge base as an archive." + }, + "deniedPartialAccessProjectIssues": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": "Issues in the listed Projects are unavailable to teammates without access to every active environment." } }, "additionalProperties": false, @@ -18200,6 +18233,16 @@ "disableKnowledgeBaseExport": { "type": "boolean", "description": "Prevent downloading a whole knowledge base as an archive." + }, + "deniedPartialAccessProjectIssues": { + "default": [], + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": "Issues in the listed Projects are unavailable to teammates without access to every active environment." } }, "required": [ @@ -18244,7 +18287,8 @@ "disableToolAutoApproval", "hideSandboxesTab", "disableOAuthAppAccess", - "disableKnowledgeBaseExport" + "disableKnowledgeBaseExport", + "deniedPartialAccessProjectIssues" ], "additionalProperties": false }, @@ -20729,7 +20773,7 @@ "description": "Access request being reviewed." }, "changes": { - "maxItems": 42, + "maxItems": 43, "type": "array", "items": { "type": "object", @@ -20778,7 +20822,8 @@ "disableToolAutoApproval", "hideSandboxesTab", "disableOAuthAppAccess", - "disableKnowledgeBaseExport" + "disableKnowledgeBaseExport", + "deniedPartialAccessProjectIssues" ], "description": "Permission restriction changed by approval." }, @@ -21290,7 +21335,7 @@ "description": "Access request being reviewed." }, "changes": { - "maxItems": 42, + "maxItems": 43, "type": "array", "items": { "type": "object", @@ -21339,7 +21384,8 @@ "disableToolAutoApproval", "hideSandboxesTab", "disableOAuthAppAccess", - "disableKnowledgeBaseExport" + "disableKnowledgeBaseExport", + "deniedPartialAccessProjectIssues" ], "description": "Permission restriction changed by approval." }, diff --git a/apps/sim/app/api/projects/[id]/route.ts b/apps/sim/app/api/projects/[id]/route.ts new file mode 100644 index 00000000000..d8e454db122 --- /dev/null +++ b/apps/sim/app/api/projects/[id]/route.ts @@ -0,0 +1,45 @@ +import { + archiveProjectContract, + getProjectContract, + renameProjectContract, +} from '@/lib/api/contracts/projects' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { + archiveProject, + getProject, + projectOperations, + renameProject, +} from '@/lib/projects/application' + +export const GET = defineInternalJsonRoute({ + contract: getProjectContract, + auth: internalSessionAuth, + operation: projectOperations.get, + rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ params, query }) => ({ projectId: params.id, ...query }), + useCase: getProject, +}) +export const PATCH = defineInternalJsonRoute({ + contract: renameProjectContract, + auth: internalSessionAuth, + operation: projectOperations.rename, + rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ params, body }) => ({ projectId: params.id, name: body.name }), + useCase: renameProject, +}) +export const DELETE = defineInternalJsonRoute({ + contract: archiveProjectContract, + auth: internalSessionAuth, + operation: projectOperations.archive, + rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ params }) => ({ projectId: params.id }), + useCase: archiveProject, +}) diff --git a/apps/sim/app/api/projects/by-workspace/[workspaceId]/route.ts b/apps/sim/app/api/projects/by-workspace/[workspaceId]/route.ts new file mode 100644 index 00000000000..d04b76ed361 --- /dev/null +++ b/apps/sim/app/api/projects/by-workspace/[workspaceId]/route.ts @@ -0,0 +1,18 @@ +import { getWorkspaceProjectContract } from '@/lib/api/contracts/projects' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { getWorkspaceProject, projectOperations } from '@/lib/projects/application' + +export const GET = defineInternalJsonRoute({ + contract: getWorkspaceProjectContract, + auth: internalSessionAuth, + operation: projectOperations.get, + rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ params }) => params, + useCase: getWorkspaceProject, +}) diff --git a/apps/sim/app/api/projects/route.ts b/apps/sim/app/api/projects/route.ts new file mode 100644 index 00000000000..fe0aca010ca --- /dev/null +++ b/apps/sim/app/api/projects/route.ts @@ -0,0 +1,18 @@ +import { listProjectsContract } from '@/lib/api/contracts/projects' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { listProjects, projectOperations } from '@/lib/projects/application' + +export const GET = defineInternalJsonRoute({ + contract: listProjectsContract, + auth: internalSessionAuth, + operation: projectOperations.list, + rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ query }) => query, + useCase: listProjects, +}) diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index 8e3e44b65b5..53ddf9390ee 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -45,6 +45,9 @@ describe('Workflows API Route - POST ordering', () => { beforeEach(() => { resetDbChainMock() + queueTableRows(schemaMock.workspace, [ + { archivedAt: null, forkSyncNewWorkflowsExcluded: false }, + ]) vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('workflow-new-id'), diff --git a/apps/sim/ee/access-control/components/group-detail.tsx b/apps/sim/ee/access-control/components/group-detail.tsx index 16e819069bd..166c83e3284 100644 --- a/apps/sim/ee/access-control/components/group-detail.tsx +++ b/apps/sim/ee/access-control/components/group-detail.tsx @@ -61,6 +61,7 @@ import { getAllBlocks } from '@/blocks' import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import type { BlockConfig } from '@/blocks/types' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' +import { ProjectIssueRestrictions } from '@/ee/access-control/components/project-issue-restrictions' import { WorkspaceSelect } from '@/ee/access-control/components/workspace-select' import { type PermissionGroup, @@ -1783,6 +1784,16 @@ export function GroupDetail({ {configTab === 'platform' && (
+ + setEditingConfig((previous) => ({ + ...previous, + deniedPartialAccessProjectIssues: value, + })) + } + />
void +} + +/** Project choices use the authorized inventory; policy remains enforced at the application boundary. */ +export function ProjectIssueRestrictions({ + organizationId, + value, + onChange, +}: ProjectIssueRestrictionsProps) { + const projects = useProjects(organizationId) + const selected = new Set(value) + return ( +
+

Restrict Issues for partial-access teammates

+

+ For selected Projects, teammates governed by this group need access to every active + environment to use Issues. +

+ {projects.isPending &&

Loading Projects…

} + {projects.error && ( +

{projects.error.message}

+ )} + {projects.data?.pages + .flatMap((page) => page.projects) + .map((project) => ( + + ))} + {projects.hasNextPage && ( + void projects.fetchNextPage()}> + Load more + + )} +
+ ) +} diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.test.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.test.ts index 00052299e3e..8c118090240 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.test.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.test.ts @@ -1,9 +1,11 @@ +import { workspace } from '@sim/db/schema' import { createBlock } from '@sim/testing/factories' +import { dbChainMock, queueTableRows, resetDbChainMock } from '@sim/testing/mocks/database.mock' import { workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, } from '@sim/testing/mocks/workflows-persistence-utils.mock' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { DbOrTx } from '@/lib/db/types' import { MAX_FOLDERS_PER_WORKSPACE } from '@/lib/folders/constants' import { FolderCollectionFullError } from '@/lib/folders/errors' @@ -23,6 +25,11 @@ import { const mockSaveWorkflowToNormalizedTables = workflowsPersistenceUtilsMockFns.mockSaveWorkflowToNormalizedTables +beforeEach(() => { + resetDbChainMock() + queueTableRows(workspace, [{ archivedAt: null, forkSyncNewWorkflowsExcluded: false }]) +}) + describe('buildWorkflowNameRegistry', () => { it('excludes the workflow itself so a replace can keep its own name', () => { const reg = buildWorkflowNameRegistry([{ id: 'w1', folderId: 'f1', name: 'Onboarding' }]) @@ -294,7 +301,7 @@ describe('copyWorkflowStateIntoTarget source tool identities', () => { async () => { mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true }) await copyWorkflowStateIntoTarget({ - tx: { insert: () => ({ values: () => Promise.resolve() }) } as unknown as DbOrTx, + tx: dbChainMock.db as unknown as DbOrTx, targetWorkflowId: 'wf-child', targetWorkspaceId: 'ws-child', userId: 'user', @@ -358,7 +365,7 @@ describe('copied MCP configuration normalization', () => { }, }) await copyWorkflowStateIntoTarget({ - tx: { insert: () => ({ values: () => Promise.resolve() }) } as unknown as DbOrTx, + tx: dbChainMock.db as unknown as DbOrTx, targetWorkflowId: 'wf-child', targetWorkspaceId: 'ws-target', userId: 'target-user', @@ -396,9 +403,7 @@ describe('copyWorkflowStateIntoTarget canonicalModes reindex propagation', () => async () => { mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true }) const seenCanonicalModes: Array | undefined> = [] - const tx = { - insert: () => ({ values: () => Promise.resolve() }), - } as unknown as DbOrTx + const tx = dbChainMock.db as unknown as DbOrTx await copyWorkflowStateIntoTarget({ tx, @@ -484,11 +489,7 @@ describe('copyWorkflowStateIntoTarget webhook path pinning', () => { resolveBlockId: (_targetWorkflowId: string, sourceBlockId: string) => `tgt-${sourceBlockId}`, } - /** `replace` mode updates the existing target workflow row; stub just that chain. */ - const stubTx = () => - ({ - update: () => ({ set: () => ({ where: () => Promise.resolve() }) }), - }) as unknown as DbOrTx + const stubTx = () => dbChainMock.db as unknown as DbOrTx function writtenSubBlocks() { const state = mockSaveWorkflowToNormalizedTables.mock.calls.at(-1)?.[1] as { @@ -549,8 +550,7 @@ describe('copyWorkflowStateIntoTarget custom-block remap', () => { resolveBlockId: (_t: string, sourceBlockId: string) => `tgt-${sourceBlockId}`, } - const stubTx = () => - ({ update: () => ({ set: () => ({ where: () => Promise.resolve() }) }) }) as unknown as DbOrTx + const stubTx = () => dbChainMock.db as unknown as DbOrTx function writtenBlock() { const state = mockSaveWorkflowToNormalizedTables.mock.calls.at(-1)?.[1] as { diff --git a/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.ts b/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.ts index 847f2ec2984..21c9c21b83d 100644 --- a/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.ts +++ b/apps/sim/ee/workspace-forking/lib/copy/copy-workflows.ts @@ -26,6 +26,7 @@ import { type SubBlockTransform, } from '@/lib/workflows/references/remap-references' import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility' +import { lockActiveWorkspace } from '@/lib/workspaces/active-workspace' import { deriveForkBlockId, type ForkBlockIdResolver, @@ -502,6 +503,7 @@ export async function copyWorkflowStateIntoTarget( requestId = 'unknown', } = params + await lockActiveWorkspace(tx, targetWorkspaceId) const targetFolderId = sourceMeta.folderId ? (folderIdMap.get(sourceMeta.folderId) ?? null) : null const varIdMapping = new Map() diff --git a/apps/sim/ee/workspace-forking/lib/create-fork.test.ts b/apps/sim/ee/workspace-forking/lib/create-fork.test.ts index 648815617f7..ff7d504868a 100644 --- a/apps/sim/ee/workspace-forking/lib/create-fork.test.ts +++ b/apps/sim/ee/workspace-forking/lib/create-fork.test.ts @@ -1,4 +1,4 @@ -import { workspace } from '@sim/db/schema' +import { projectWorkspace, workspace } from '@sim/db/schema' import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { workflowsPersistenceUtilsMock } from '@sim/testing/mocks/workflows-persistence-utils.mock' import { @@ -140,12 +140,17 @@ function forkParams(selection?: { describe('createFork storage headroom gate', () => { beforeEach(() => { resetDbChainMock() + queueTableRows(projectWorkspace, [{ projectId: 'project-source' }]) + queueTableRows(projectWorkspace, [ + { project: { id: 'project-source', organizationId: null, archivedAt: null } }, + ]) /** * The fork transaction re-reads the parent's organization under the lock to * confirm it has not moved since `assertCanFork` captured the policy. * Matches POLICY.organizationId, so the fork proceeds. */ queueTableRows(workspace, [{ organizationId: null }]) + queueTableRows(workspace, [{ archivedAt: null, forkSyncNewWorkflowsExcluded: false }]) mockSumForkCopyBytes.mockResolvedValue(0) mockAssertForkStorageHeadroom.mockResolvedValue(undefined) mockLoadSourceDeployedStates.mockResolvedValue({ @@ -210,6 +215,10 @@ describe('createFork storage headroom gate', () => { it('refuses when the parent changed organizations after the policy was captured', async () => { resetDbChainMock() + queueTableRows(projectWorkspace, [{ projectId: 'project-source' }]) + queueTableRows(projectWorkspace, [ + { project: { id: 'project-source', organizationId: null, archivedAt: null } }, + ]) /** * `assertCanFork` captures `policy.organizationId` before this transaction, * so an admin workspace move committing in between would otherwise leave @@ -234,7 +243,12 @@ describe('createFork storage headroom gate', () => { */ it('gives the child the source workspace personal API-key and fork-sync policies', async () => { resetDbChainMock() + queueTableRows(projectWorkspace, [{ projectId: 'project-source' }]) + queueTableRows(projectWorkspace, [ + { project: { id: 'project-source', organizationId: null, archivedAt: null } }, + ]) queueTableRows(workspace, [{ organizationId: null, forkSyncNewWorkflowsExcluded: true }]) + queueTableRows(workspace, [{ archivedAt: null, forkSyncNewWorkflowsExcluded: true }]) const result = await createFork(forkParams()) diff --git a/apps/sim/ee/workspace-forking/lib/create-fork.ts b/apps/sim/ee/workspace-forking/lib/create-fork.ts index 8b8a06bbb77..1192b8985f2 100644 --- a/apps/sim/ee/workspace-forking/lib/create-fork.ts +++ b/apps/sim/ee/workspace-forking/lib/create-fork.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { permissions, workflow, workspace } from '@sim/db/schema' +import { permissions, projectWorkspace, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import type { PermissionType } from '@sim/platform-authz/workspace' import { getErrorMessage } from '@sim/utils/errors' @@ -7,6 +7,7 @@ import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import type { Workspace } from '@/lib/api/contracts/workspaces' import { enqueueOutboxEvent } from '@/lib/core/outbox/service' +import { requireForkProject } from '@/lib/projects/membership' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -231,6 +232,7 @@ export async function createFork(params: CreateForkParams): Promise workspaceForkin import { unlinkForkEdge } from '@/ee/workspace-forking/lib/lineage/unlink' -const { mockSetForkLockTimeout, mockAcquireForkEdgeLock } = workspaceForkingLineageMockFns const { mockResolveForkLineageRootId } = workspaceForkingLineageRootMockFns const EDGE = { childWorkspaceId: 'child-ws', parentWorkspaceId: 'parent-ws' } @@ -27,21 +23,6 @@ describe('unlinkForkEdge', () => { mockResolveForkLineageRootId.mockResolvedValue('root-ws') }) - it('nulls the child pointer and purges all four edge tables under the edge lock', async () => { - dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'child-ws' }]) - - const result = await unlinkForkEdge(EDGE, 'req-1') - - expect(result).toEqual({ unlinked: true }) - expect(mockSetForkLockTimeout).toHaveBeenCalledTimes(1) - expect(mockAcquireForkEdgeLock).toHaveBeenCalledWith(dbChainMock.db, 'child-ws') - expect(dbChainMockFns.update).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.set).toHaveBeenCalledWith( - expect.objectContaining({ forkedFromWorkspaceId: null }) - ) - expect(dbChainMockFns.delete).toHaveBeenCalledTimes(4) - }) - /** A root moved by a concurrent unlink higher up means the lock taken no longer covers it. */ it('refuses when the lineage root moved before the lock was taken', async () => { mockResolveForkLineageRootId.mockResolvedValueOnce('root-ws').mockResolvedValueOnce('parent-ws') diff --git a/apps/sim/ee/workspace-forking/lib/lineage/unlink.ts b/apps/sim/ee/workspace-forking/lib/lineage/unlink.ts index 6540a3c4a96..e7e610dc20b 100644 --- a/apps/sim/ee/workspace-forking/lib/lineage/unlink.ts +++ b/apps/sim/ee/workspace-forking/lib/lineage/unlink.ts @@ -8,6 +8,7 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' +import { splitForkProject } from '@/lib/projects/membership' import { ForkError } from '@/ee/workspace-forking/lib/lineage/authz' import { acquireForkEdgeLock, @@ -27,8 +28,7 @@ export interface UnlinkForkResult { /** * Permanently dissolve a fork edge: null the child's `forkedFromWorkspaceId` (the * edge's single source of truth) and purge the edge's fork state — resource map, - * block map, dependent values, and promote-run undo points. Both workspaces are - * left untouched; only the association and its metadata are removed. + * block map, dependent values, and promote-run undo points. Both workspaces remain; the detached subtree moves into its own Project. * * Runs in one transaction under the lineage and edge advisory locks. Every promote and * rollback on the edge holds the edge lock, so an in-flight sync either finishes before @@ -62,6 +62,14 @@ export async function unlinkForkEdge( } await acquireForkEdgeLock(tx, childWorkspaceId) + const [currentEdge] = await tx + .select({ parentId: workspace.forkedFromWorkspaceId }) + .from(workspace) + .where(eq(workspace.id, childWorkspaceId)) + .limit(1) + if (currentEdge?.parentId !== parentWorkspaceId) return false + await splitForkProject(tx, childWorkspaceId) + const updated = await tx .update(workspace) .set({ forkedFromWorkspaceId: null, updatedAt: new Date() }) diff --git a/apps/sim/hooks/queries/projects.ts b/apps/sim/hooks/queries/projects.ts new file mode 100644 index 00000000000..c02e8c28353 --- /dev/null +++ b/apps/sim/hooks/queries/projects.ts @@ -0,0 +1,25 @@ +import { useInfiniteQuery } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import { listProjectsContract } from '@/lib/api/contracts/projects' + +const PROJECT_LIST_STALE_TIME = 30_000 +const projectKeys = { + all: ['projects'] as const, + lists: () => [...projectKeys.all, 'list'] as const, + list: (organizationId: string) => [...projectKeys.lists(), organizationId] as const, +} + +export function useProjects(organizationId: string) { + return useInfiniteQuery({ + queryKey: projectKeys.list(organizationId), + initialPageParam: null as string | null, + queryFn: ({ signal, pageParam }) => + requestJson(listProjectsContract, { + query: { organizationId, cursor: pageParam ?? undefined, limit: 100 }, + signal, + }), + getNextPageParam: (page) => page.nextCursor, + staleTime: PROJECT_LIST_STALE_TIME, + enabled: Boolean(organizationId), + }) +} diff --git a/apps/sim/lib/api/contracts/projects.ts b/apps/sim/lib/api/contracts/projects.ts new file mode 100644 index 00000000000..85e774a9cd6 --- /dev/null +++ b/apps/sim/lib/api/contracts/projects.ts @@ -0,0 +1,86 @@ +import { z } from 'zod' +import { nonEmptyIdSchema } from '@/lib/api/contracts/primitives' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const projectEnvironmentSchema = z.object({ + id: nonEmptyIdSchema, + name: z.string(), + forkedFromWorkspaceId: nonEmptyIdSchema.nullable(), +}) +export type ProjectEnvironment = z.output +export const projectSchema = z.object({ + id: nonEmptyIdSchema, + name: z.string(), + organizationId: nonEmptyIdSchema.nullable(), + ownerId: nonEmptyIdSchema, + archivedAt: z.coerce.date().nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + environments: z.array(projectEnvironmentSchema), + capabilities: z.object({ administer: z.boolean(), issues: z.boolean() }), +}) +export type Project = z.output +export const projectParamsSchema = z.object({ id: nonEmptyIdSchema }) +export type ProjectParams = z.input +export const projectQuerySchema = z.object({ + organizationId: nonEmptyIdSchema.optional(), + workspaceId: nonEmptyIdSchema.optional(), +}) +export type ProjectQuery = z.input +export const listProjectsQuerySchema = z.object({ + organizationId: nonEmptyIdSchema.optional(), + cursor: nonEmptyIdSchema.optional(), + limit: z.coerce.number().int().min(1).max(100).default(50), +}) +export type ListProjectsQuery = z.input +export const listProjectsResponseSchema = z.object({ + projects: z.array(projectSchema), + nextCursor: nonEmptyIdSchema.nullable(), +}) +export type ListProjectsResponse = z.output +export const listProjectsContract = defineRouteContract({ + method: 'GET', + path: '/api/projects', + query: listProjectsQuerySchema, + response: { mode: 'json', schema: listProjectsResponseSchema }, +}) +export const getProjectResponseSchema = z.object({ project: projectSchema }) +export type GetProjectResponse = z.output +export const getProjectContract = defineRouteContract({ + method: 'GET', + path: '/api/projects/[id]', + params: projectParamsSchema, + query: projectQuerySchema, + response: { mode: 'json', schema: getProjectResponseSchema }, +}) +export const renameProjectBodySchema = z.object({ name: z.string().trim().min(1).max(100) }) +export type RenameProjectBody = z.input +export const renameProjectResponseSchema = z.object({ id: nonEmptyIdSchema, name: z.string() }) +export type RenameProjectResponse = z.output +export const renameProjectContract = defineRouteContract({ + method: 'PATCH', + path: '/api/projects/[id]', + params: projectParamsSchema, + body: renameProjectBodySchema, + response: { mode: 'json', schema: renameProjectResponseSchema }, +}) +export const archiveProjectResponseSchema = z.object({ + id: nonEmptyIdSchema, + archived: z.boolean(), +}) +export type ArchiveProjectResponse = z.output +export const archiveProjectContract = defineRouteContract({ + method: 'DELETE', + path: '/api/projects/[id]', + params: projectParamsSchema, + response: { mode: 'json', schema: archiveProjectResponseSchema }, +}) + +export const workspaceProjectParamsSchema = z.object({ workspaceId: nonEmptyIdSchema }) +export type WorkspaceProjectParams = z.input +export const getWorkspaceProjectContract = defineRouteContract({ + method: 'GET', + path: '/api/projects/by-workspace/[workspaceId]', + params: workspaceProjectParamsSchema, + response: { mode: 'json', schema: getProjectResponseSchema }, +}) diff --git a/apps/sim/lib/billing/organizations/lock-order.test.ts b/apps/sim/lib/billing/organizations/lock-order.test.ts index 11aa771177f..108bb594f63 100644 --- a/apps/sim/lib/billing/organizations/lock-order.test.ts +++ b/apps/sim/lib/billing/organizations/lock-order.test.ts @@ -228,6 +228,7 @@ describe('workspace payer-change transaction lock ordering', () => { const tx = { execute: async () => [], select, + selectDistinct: select, insert: () => ({ values: () => ({ onConflictDoUpdate: async () => undefined, diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index ce5181d8a27..ebbdbb0b0fe 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -15,6 +15,7 @@ import { organization, permissionGroupMember, permissions, + project, subscription as subscriptionTable, user, userStats, @@ -60,6 +61,7 @@ import { revokePersonalApiKeysTx, revokeUserSessionsTx, } from '@/lib/organizations/members/revocation' +import { tryLockProject } from '@/lib/projects/membership' import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, @@ -555,6 +557,25 @@ async function reassignOwnedOrganizationResourcesTx({ const ownerId = ownerMembership?.userId if (!ownerId || ownerId === userId) return 0 + const ownedProjects = await tx + .select({ id: project.id }) + .from(project) + .where(and(eq(project.organizationId, organizationId), eq(project.ownerId, userId))) + .orderBy(project.id) + for (const row of ownedProjects) { + await tryLockProject(tx, row.id) + await tx + .update(project) + .set({ ownerId, updatedAt: new Date() }) + .where( + and( + eq(project.id, row.id), + eq(project.ownerId, userId), + eq(project.organizationId, organizationId) + ) + ) + } + /** Creator attribution must survive account deletion without changing document ACLs. */ await tx .update(knowledgeBase) diff --git a/apps/sim/lib/permission-groups/capabilities.ts b/apps/sim/lib/permission-groups/capabilities.ts index 8260dea111d..ab3743d91a2 100644 --- a/apps/sim/lib/permission-groups/capabilities.ts +++ b/apps/sim/lib/permission-groups/capabilities.ts @@ -58,6 +58,7 @@ export const CAPABILITY_IDS = [ 'copilot.tool_auto_approval', 'sandboxes.use', 'knowledge.export', + 'project_issues.use', ] as const export type PermissionGroupCapability = (typeof CAPABILITY_IDS)[number] @@ -431,6 +432,14 @@ export const CAPABILITY_RULES = { describe: 'Exporting a knowledge base', deniedBy: (config) => config.disableKnowledgeBaseExport || config.hideKnowledgeBaseTab, }, + 'project_issues.use': { + kind: 'parameterized', + configKeys: ['deniedPartialAccessProjectIssues'], + detailCode: 'PERMISSION_GROUP_CAPABILITY_BLOCKED', + describe: 'Project Issues access', + deniedBy: (config: PermissionGroupConfig, projectId: string) => + config.deniedPartialAccessProjectIssues.includes(projectId), + }, } satisfies { readonly [K in PermissionGroupCapability]: CapabilityRule } /** diff --git a/apps/sim/lib/permission-groups/fields.test.ts b/apps/sim/lib/permission-groups/fields.test.ts index 45f99a25312..bd3070defc5 100644 --- a/apps/sim/lib/permission-groups/fields.test.ts +++ b/apps/sim/lib/permission-groups/fields.test.ts @@ -163,6 +163,7 @@ const fixtures: readonly CoercionFixture[] = [ hideSandboxesTab: true, disableOAuthAppAccess: true, disableKnowledgeBaseExport: true, + deniedPartialAccessProjectIssues: ['project-a'], }, expected: { allowedIntegrations: ['slack_v2'], @@ -207,6 +208,7 @@ const fixtures: readonly CoercionFixture[] = [ hideSandboxesTab: true, disableOAuthAppAccess: true, disableKnowledgeBaseExport: true, + deniedPartialAccessProjectIssues: ['project-a'], }, }, ] diff --git a/apps/sim/lib/permission-groups/fields.ts b/apps/sim/lib/permission-groups/fields.ts index 8bb73786daf..0ceb1036c70 100644 --- a/apps/sim/lib/permission-groups/fields.ts +++ b/apps/sim/lib/permission-groups/fields.ts @@ -506,6 +506,11 @@ export const PERMISSION_GROUP_FIELDS = { category: 'Knowledge Base', hint: 'Prevent downloading a whole knowledge base as an archive.', }), + deniedPartialAccessProjectIssues: denylist( + z.string().min(1).max(255), + 'capability', + 'Issues in the listed Projects are unavailable to teammates without access to every active environment.' + ), } satisfies Record export type PermissionGroupFields = typeof PERMISSION_GROUP_FIELDS diff --git a/apps/sim/lib/permission-groups/group-manager.ts b/apps/sim/lib/permission-groups/group-manager.ts index 3c492e42f09..db529c1ddfa 100644 --- a/apps/sim/lib/permission-groups/group-manager.ts +++ b/apps/sim/lib/permission-groups/group-manager.ts @@ -1,7 +1,12 @@ import { db } from '@sim/db' -import { permissionGroup, permissionGroupMember, permissionGroupWorkspace } from '@sim/db/schema' +import { + permissionGroup, + permissionGroupMember, + permissionGroupWorkspace, + project, +} from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx } from '@/lib/db/types' import { @@ -54,6 +59,23 @@ async function validateWorkspaces( ) } +async function validateProjectRestrictions( + organizationId: string, + ids: string[] | undefined, + tx: DbOrTx +) { + if (!ids?.length) return + const projects = await tx + .select({ id: project.id }) + .from(project) + .where(and(eq(project.organizationId, organizationId), inArray(project.id, ids))) + if (projects.length !== new Set(ids).size) + throw new OrchestrationError( + 'validation', + 'A restricted Project does not belong to this organization' + ) +} + async function assertAvailableName( organizationId: string, name: string, @@ -115,6 +137,11 @@ export async function createPermissionGroupRecord( 'Select at least one workspace when the group targets specific workspaces' ) return withPermissionGroupMutation(organizationId, async (tx) => { + await validateProjectRestrictions( + organizationId, + input.config?.deniedPartialAccessProjectIssues, + tx + ) await validateWorkspaces(organizationId, workspaceIds, tx) await assertAvailableName(organizationId, input.name, tx) const now = new Date() @@ -151,6 +178,11 @@ export async function updatePermissionGroupRecord( updates: PermissionGroupChanges ) { return withPermissionGroupMutation(organizationId, async (tx) => { + await validateProjectRestrictions( + organizationId, + updates.config?.deniedPartialAccessProjectIssues, + tx + ) const group = await requirePermissionGroup(organizationId, groupId, tx) if (updates.name !== undefined) await assertAvailableName(organizationId, updates.name, tx, groupId) diff --git a/apps/sim/lib/projects/__integration__/foundation.integration.ts b/apps/sim/lib/projects/__integration__/foundation.integration.ts new file mode 100644 index 00000000000..0d8cdd291d5 --- /dev/null +++ b/apps/sim/lib/projects/__integration__/foundation.integration.ts @@ -0,0 +1,619 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { db } from '@sim/db' +import { + member, + organization, + permissionGroup, + permissions, + project, + projectWorkspace, + user, + userStats, + workflow, + workspace, + workspaceForkBlockMap, + workspaceForkDependentValue, + workspaceForkPromoteRun, + workspaceForkResourceMap, +} from '@sim/db/schema' +import { + createSessionPrincipal, + createWorkspaceApiKeyPrincipal, +} from '@sim/testing/factories/principal.factory' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { afterAll, describe, expect, it } from 'vitest' +import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' +import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion' +import { + archiveProject, + getProject, + getProjectIssueAccess, + getWorkspaceProject, + listProjects, + renameProject, +} from '@/lib/projects/application' +import { archiveProjectInTransaction } from '@/lib/projects/lifecycle' +import { createProjectForWorkspace, transferWorkspaceProjects } from '@/lib/projects/membership' +import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' +import { createWorkspaceInTransaction } from '@/lib/workspaces/create' +import { archiveWorkspace } from '@/lib/workspaces/lifecycle' +import { detachOrganizationWorkspacesTx } from '@/lib/workspaces/organization-workspaces' +import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' +import { getWorkspaceCreationPolicy } from '@/lib/workspaces/policy' +import { createFork } from '@/ee/workspace-forking/lib/create-fork' +import { unlinkForkEdge } from '@/ee/workspace-forking/lib/lineage/unlink' + +const users: string[] = [] +const organizations: string[] = [] +const environments: string[] = [] +const request = { requestId: 'project-foundation-integration', headers: new Headers() } +const checks: { name: string; status: 'passed' | 'failed'; durationMs: number; error?: string }[] = + [] + +/** Exercises durable auth and lifecycle invariants against real Postgres, including concurrent writers. */ +function check(name: string, run: () => Promise) { + it(name, async () => { + const started = performance.now() + try { + await run() + checks.push({ name, status: 'passed', durationMs: performance.now() - started }) + } catch (error) { + checks.push({ + name, + status: 'failed', + durationMs: performance.now() - started, + error: getErrorMessage(error), + }) + throw error + } + }) +} + +async function fixture(org = true, count = 2) { + const ownerId = generateId() + const teammateId = generateId() + const outsiderId = generateId() + const now = new Date() + for (const id of [ownerId, teammateId, outsiderId]) { + users.push(id) + await db.insert(user).values({ + id, + email: `${id}@projects.invalid`, + name: 'Project fixture', + emailVerified: true, + createdAt: now, + updatedAt: now, + }) + } + await db.insert(userStats).values({ id: generateId(), userId: ownerId }) + const organizationId = org ? generateId() : null + if (organizationId) { + organizations.push(organizationId) + await db + .insert(organization) + .values({ id: organizationId, name: 'Project fixture', slug: organizationId, createdAt: now }) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: ownerId, role: 'owner', createdAt: now }) + } + const ids = Array.from({ length: count }, () => generateId()) + let projectId = '' + await db.transaction(async (tx) => { + for (const [index, id] of ids.entries()) { + environments.push(id) + await tx.insert(workspace).values({ + id, + name: `Environment ${index}`, + ownerId, + billedAccountUserId: ownerId, + organizationId, + workspaceMode: organizationId ? 'organization' : 'grandfathered_shared', + forkedFromWorkspaceId: index ? ids[index - 1] : null, + }) + await tx.insert(permissions).values({ + id: generateId(), + entityType: 'workspace', + entityId: id, + userId: ownerId, + permissionType: 'admin', + }) + if (!index) + projectId = await createProjectForWorkspace(tx, { + workspaceId: id, + name: 'Environment 0', + organizationId, + ownerId, + }) + else await tx.insert(projectWorkspace).values({ projectId, workspaceId: id }) + } + }) + const owner = createSessionPrincipal({ userId: ownerId }) + const teammate = createSessionPrincipal({ userId: teammateId }) + await db.insert(permissions).values({ + id: generateId(), + entityType: 'workspace', + entityId: ids[0], + userId: teammateId, + permissionType: 'admin', + }) + return { ownerId, teammateId, outsiderId, organizationId, projectId, ids, owner, teammate } +} + +async function addWorkflow(workspaceId: string, userId: string) { + const id = generateId() + const now = new Date() + await db.insert(workflow).values({ + id, + workspaceId, + userId, + name: 'Scheduled work', + createdAt: now, + updatedAt: now, + lastSynced: now, + isDeployed: true, + isPublicApi: true, + }) + return id +} + +afterAll(async () => { + const reportPath = + process.env.PROJECT_FOUNDATION_REPORT_PATH ?? resolve('test-results/project-foundation.json') + await mkdir(dirname(reportPath), { recursive: true }) + await writeFile(reportPath, JSON.stringify({ checks }, null, 2)) + if (environments.length) { + const memberships = await db + .select({ id: projectWorkspace.projectId }) + .from(projectWorkspace) + .where(inArray(projectWorkspace.workspaceId, environments)) + const ids = [...new Set(memberships.map((row) => row.id))] + await db.delete(projectWorkspace).where(inArray(projectWorkspace.workspaceId, environments)) + if (ids.length) await db.delete(project).where(inArray(project.id, ids)) + await db.delete(workspace).where(inArray(workspace.id, environments)) + } + if (organizations.length) + await db.delete(organization).where(inArray(organization.id, organizations)) + if (users.length) await db.delete(user).where(inArray(user.id, users)) +}) + +describe('Project foundation at the database and application boundary', () => { + check('workspace creation and forks commit exactly one Project membership', async () => { + const f = await fixture(false, 1) + const source = await db.transaction((tx) => + createWorkspaceInTransaction(tx, { + userId: f.ownerId, + name: 'New environment', + organizationId: null, + observedOrganizationId: null, + governingPermissionGroupOrganizationId: null, + workspaceMode: 'personal', + billedAccountUserId: f.ownerId, + skipDefaultWorkflow: true, + }) + ) + environments.push(source.id) + const before = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, source.id)) + expect(before).toHaveLength(1) + const policy = await getWorkspaceCreationPolicy({ userId: f.ownerId }) + const parent = await getWorkspaceWithOwner(source.id) + if (!parent) throw new Error('Missing source fixture') + const fork = await createFork({ + source: parent, + policy, + userId: f.ownerId, + name: 'Child environment', + }) + environments.push(fork.workspace.id) + const child = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, fork.workspace.id)) + expect(child).toHaveLength(1) + expect(child[0].projectId).toBe(before[0].projectId) + }) + + check( + 'a departing organization member transfers Project lifecycle ownership to the org owner', + async () => { + const f = await fixture() + const organizationId = f.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + const memberId = generateId() + await db.insert(member).values({ + id: memberId, + organizationId: organizationId, + userId: f.teammateId, + role: 'member', + createdAt: new Date(), + }) + await db.update(project).set({ ownerId: f.teammateId }).where(eq(project.id, f.projectId)) + const result = await removeUserFromOrganization({ + userId: f.teammateId, + organizationId: organizationId, + memberId, + actorUserId: f.ownerId, + skipBillingLogic: true, + onError: 'throw', + }) + expect(result.success).toBe(true) + const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) + expect(record.ownerId).toBe(f.ownerId) + expect(record.organizationId).toBe(f.organizationId) + } + ) + + check( + 'hides inaccessible environments and denies mismatched scopes and workspace keys', + async () => { + const f = await fixture() + const input = { projectId: f.projectId } + const result = await getProject.execute({ principal: f.teammate, input, request }) + expect(result.project.environments.map((row) => row.id)).toEqual([f.ids[0]]) + expect(result.project.capabilities).toEqual({ administer: false, issues: true }) + await expect( + getWorkspaceProject.execute({ + principal: f.teammate, + input: { workspaceId: f.ids[1] }, + request, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + await expect( + getProject.execute({ + principal: f.teammate, + input: { ...input, organizationId: generateId() }, + request, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + await expect( + getProject.execute({ + principal: createSessionPrincipal({ userId: f.outsiderId }), + input, + request, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + await expect( + getProject.execute({ + principal: createWorkspaceApiKeyPrincipal({ workspaceId: f.ids[0] }), + input, + request, + }) + ).rejects.toThrow() + await expect( + renameProject.execute({ + principal: f.teammate, + input: { ...input, name: 'Renamed' }, + request, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + const list = await listProjects.execute({ + principal: f.teammate, + input: { limit: 1 }, + request, + }) + expect(list.projects.map((row) => row.id)).toEqual([f.projectId]) + } + ) + + check( + 'Issues restriction applies only to partial access and excludes archived environments', + async () => { + const f = await fixture() + const organizationId = f.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + const args = { principal: f.teammate, input: { projectId: f.projectId }, request } + await expect(getProjectIssueAccess.execute(args)).resolves.toEqual({ projectId: f.projectId }) + await db.insert(permissionGroup).values({ + id: generateId(), + organizationId: organizationId, + name: 'Default restrictions', + createdBy: f.ownerId, + isDefault: true, + config: { deniedPartialAccessProjectIssues: [f.projectId] }, + }) + await expect(getProjectIssueAccess.execute(args)).rejects.toMatchObject({ + detailCode: 'PERMISSION_GROUP_CAPABILITY_BLOCKED', + }) + await db.insert(permissions).values({ + id: generateId(), + userId: f.teammateId, + entityType: 'workspace', + entityId: f.ids[1], + permissionType: 'read', + }) + await expect(getProjectIssueAccess.execute(args)).resolves.toEqual({ projectId: f.projectId }) + await db + .delete(permissions) + .where(and(eq(permissions.userId, f.teammateId), eq(permissions.entityId, f.ids[1]))) + await archiveWorkspace(f.ids[1], request) + await expect(getProjectIssueAccess.execute(args)).resolves.toEqual({ projectId: f.projectId }) + await expect( + renameProject.execute({ + ...args, + input: { ...args.input, name: 'Still requires all admins' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + } + ) + + check( + 'personal ownership does not substitute for access; admins of every environment may rename', + async () => { + const f = await fixture(false) + await db.insert(permissions).values({ + id: generateId(), + userId: f.teammateId, + entityType: 'workspace', + entityId: f.ids[1], + permissionType: 'admin', + }) + await expect( + renameProject.execute({ + principal: f.teammate, + input: { projectId: f.projectId, name: 'Personal project' }, + request, + }) + ).resolves.toEqual({ id: f.projectId, name: 'Personal project' }) + await db.delete(permissions).where(eq(permissions.userId, f.ownerId)) + await expect( + getProject.execute({ principal: f.owner, input: { projectId: f.projectId }, request }) + ).rejects.toMatchObject({ code: 'not_found' }) + } + ) + + check('concurrent individual removals preserve the last active environment', async () => { + const f = await fixture(false) + const results = await Promise.allSettled(f.ids.map((id) => archiveWorkspace(id, request))) + expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1) + expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1) + const rows = await db.select().from(workspace).where(inArray(workspace.id, f.ids)) + expect(rows.filter((row) => !row.archivedAt)).toHaveLength(1) + }) + + check( + 'retrying an environment archive repairs previously unarchived child workflows', + async () => { + const f = await fixture(false) + await archiveWorkspace(f.ids[1], request) + const workflowId = await addWorkflow(f.ids[1], f.ownerId) + expect(await archiveWorkspace(f.ids[1], request)).toMatchObject({ archived: false }) + const [row] = await db.select().from(workflow).where(eq(workflow.id, workflowId)) + expect(row.archivedAt).not.toBeNull() + expect(row.isDeployed).toBe(false) + expect(row.isPublicApi).toBe(false) + } + ) + + check( + 'Project archive rolls back as a unit and retries without losing environments', + async () => { + const f = await fixture() + const workflowIds = await Promise.all(f.ids.map((id) => addWorkflow(id, f.ownerId))) + await expect( + db.transaction(async (tx) => { + await archiveProjectInTransaction(tx, f.projectId) + throw new Error('Abort compound archive') + }) + ).rejects.toThrow('Abort compound archive') + const before = await db.select().from(workspace).where(inArray(workspace.id, f.ids)) + expect(before.every((row) => row.archivedAt === null)).toBe(true) + const args = { principal: f.owner, input: { projectId: f.projectId }, request } + await archiveProject.execute(args) + await archiveProject.execute(args) + await expect( + db.transaction(async (tx) => { + const row = await buildNewWorkflowRow(tx, { + id: generateId(), + userId: f.ownerId, + workspaceId: f.ids[0], + folderId: null, + name: 'Late workflow', + description: null, + }) + await tx.insert(workflow).values(row) + }) + ).rejects.toMatchObject({ code: 'not_found' }) + + const rows = await db.select().from(workspace).where(inArray(workspace.id, f.ids)) + expect(rows.every((row) => row.archivedAt !== null)).toBe(true) + const workflows = await db.select().from(workflow).where(inArray(workflow.id, workflowIds)) + expect( + workflows.every((row) => row.archivedAt !== null && !row.isDeployed && !row.isPublicApi) + ).toBe(true) + expect( + await db.select().from(projectWorkspace).where(eq(projectWorkspace.projectId, f.projectId)) + ).toHaveLength(2) + await expect(getProjectIssueAccess.execute(args)).rejects.toMatchObject({ code: 'conflict' }) + } + ) + + check( + 'disconnect moves descendants to one new Project and preserves restrictions on retry', + async () => { + const f = await fixture(true, 3) + const organizationId = f.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + const groupId = generateId() + await db.insert(permissionGroup).values({ + id: groupId, + organizationId: organizationId, + name: 'Restricted', + createdBy: f.ownerId, + isDefault: true, + config: { deniedPartialAccessProjectIssues: [f.projectId] }, + }) + for (const childWorkspaceId of [f.ids[1], f.ids[2]]) { + await db.insert(workspaceForkResourceMap).values({ + id: generateId(), + childWorkspaceId, + resourceType: 'workflow', + parentResourceId: 'parent-workflow', + childResourceId: 'child-workflow', + }) + await db.insert(workspaceForkBlockMap).values({ + id: generateId(), + childWorkspaceId, + parentWorkflowId: 'parent-workflow', + childWorkflowId: 'child-workflow', + parentBlockId: 'parent-block', + childBlockId: 'child-block', + }) + await db.insert(workspaceForkDependentValue).values({ + id: generateId(), + childWorkspaceId, + targetWorkflowId: 'child-workflow', + targetBlockId: 'child-block', + subBlockKey: 'selection', + value: 'saved-selection', + }) + await db.insert(workspaceForkPromoteRun).values({ + id: generateId(), + childWorkspaceId, + sourceWorkspaceId: f.ids[0], + targetWorkspaceId: childWorkspaceId, + direction: 'pull', + snapshot: {}, + }) + } + const edge = { parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] } + expect(await unlinkForkEdge(edge)).toEqual({ unlinked: true }) + for (const table of [ + workspaceForkResourceMap, + workspaceForkBlockMap, + workspaceForkDependentValue, + workspaceForkPromoteRun, + ]) { + const rows = await db + .select({ childWorkspaceId: table.childWorkspaceId }) + .from(table) + .where(inArray(table.childWorkspaceId, [f.ids[1], f.ids[2]])) + expect(rows).toEqual([{ childWorkspaceId: f.ids[2] }]) + } + const first = await db + .select() + .from(projectWorkspace) + .where(inArray(projectWorkspace.workspaceId, f.ids)) + const root = first.find((row) => row.workspaceId === f.ids[0]) + const child = first.find((row) => row.workspaceId === f.ids[1]) + const grandchild = first.find((row) => row.workspaceId === f.ids[2]) + if (!root || !child || !grandchild) throw new Error('Missing family membership fixture') + expect(root.projectId).toBe(f.projectId) + expect(child.projectId).not.toBe(f.projectId) + expect(grandchild.projectId).toBe(child.projectId) + expect(await unlinkForkEdge(edge)).toEqual({ unlinked: false }) + const [group] = await db.select().from(permissionGroup).where(eq(permissionGroup.id, groupId)) + expect(group.config).toEqual({ + deniedPartialAccessProjectIssues: [f.projectId, child.projectId], + }) + const second = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, f.ids[1])) + expect(second[0].projectId).toBe(child.projectId) + } + ) + + check( + 'disconnect refuses a partially assigned subtree without moving any environments', + async () => { + const f = await fixture(true, 3) + await db.delete(projectWorkspace).where(eq(projectWorkspace.workspaceId, f.ids[2])) + await expect( + unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] }) + ).rejects.toMatchObject({ code: 'conflict' }) + const [child] = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, f.ids[1])) + expect(child.projectId).toBe(f.projectId) + const [edge] = await db.select().from(workspace).where(eq(workspace.id, f.ids[1])) + expect(edge.forkedFromWorkspaceId).toBe(f.ids[0]) + } + ) + + check( + 'organization moves remove obsolete Project restrictions while preserving unrelated policy', + async () => { + const source = await fixture(true, 1) + const organizationId = source.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + const destination = await fixture(true, 1) + const groupId = generateId() + await db.insert(permissionGroup).values({ + id: groupId, + organizationId: organizationId, + name: 'Source policy', + createdBy: source.ownerId, + isDefault: true, + config: { deniedPartialAccessProjectIssues: [source.projectId], hideTablesTab: true }, + }) + await db.transaction(async (tx) => { + await transferWorkspaceProjects(tx, source.ids, destination.organizationId) + await tx + .update(workspace) + .set({ organizationId: destination.organizationId }) + .where(eq(workspace.id, source.ids[0])) + }) + const [group] = await db.select().from(permissionGroup).where(eq(permissionGroup.id, groupId)) + expect(group.config).toEqual({ deniedPartialAccessProjectIssues: [], hideTablesTab: true }) + const [moved] = await db.select().from(project).where(eq(project.id, source.projectId)) + expect(moved).toMatchObject({ + organizationId: destination.organizationId, + ownerId: source.ownerId, + }) + } + ) + + check( + 'organization deletion preserves Project identity and assigns its former owner explicitly', + async () => { + const f = await fixture() + const organizationId = f.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + await db.transaction(async (tx) => { + await detachOrganizationWorkspacesTx(tx, organizationId) + await tx.delete(organization).where(eq(organization.id, organizationId)) + }) + const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) + expect(record).toMatchObject({ id: f.projectId, organizationId: null, ownerId: f.ownerId }) + const result = await getProject.execute({ + principal: f.owner, + input: { projectId: f.projectId }, + request, + }) + expect(result.project.environments).toHaveLength(2) + await expect( + db.transaction((tx) => transferWorkspaceProjects(tx, [f.ids[0]], generateId())) + ).rejects.toMatchObject({ code: 'conflict' }) + } + ) + + check( + 'account teardown transfers surviving Projects and atomically removes wholly private Projects', + async () => { + const f = await fixture(false) + await db.insert(permissions).values({ + id: generateId(), + userId: f.teammateId, + entityType: 'workspace', + entityId: f.ids[1], + permissionType: 'admin', + }) + await db.transaction((tx) => prepareProjectsForAccountDeletion(tx, f.ownerId, [f.ids[0]])) + const [survivor] = await db.select().from(project).where(eq(project.id, f.projectId)) + expect(survivor.ownerId).toBe(f.teammateId) + const privateProject = await fixture(false, 1) + await db.transaction((tx) => + prepareProjectsForAccountDeletion(tx, privateProject.ownerId, privateProject.ids) + ) + expect( + await db.select().from(project).where(eq(project.id, privateProject.projectId)) + ).toEqual([]) + } + ) +}) diff --git a/apps/sim/lib/projects/account-deletion.ts b/apps/sim/lib/projects/account-deletion.ts new file mode 100644 index 00000000000..541ea922767 --- /dev/null +++ b/apps/sim/lib/projects/account-deletion.ts @@ -0,0 +1,83 @@ +import { permissions, project, projectWorkspace, workspace } from '@sim/db/schema' +import { and, asc, eq, inArray, ne, or, sql } from 'drizzle-orm' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbTransaction } from '@/lib/db/types' +import { lockProject } from '@/lib/projects/membership' + +/** Account teardown may erase a wholly private Project, but never strand a surviving one. */ +export async function prepareProjectsForAccountDeletion( + tx: DbTransaction, + userId: string, + doomedWorkspaceIds: string[] +): Promise { + const records = await tx + .select({ id: project.id }) + .from(project) + .where( + or( + eq(project.ownerId, userId), + doomedWorkspaceIds.length + ? sql`${project.id} in ( + select ${projectWorkspace.projectId} from ${projectWorkspace} + where ${inArray(projectWorkspace.workspaceId, doomedWorkspaceIds)} + )` + : undefined + ) + ) + .orderBy(asc(project.id)) + const doomed = new Set(doomedWorkspaceIds) + for (const { id } of records) { + await lockProject(tx, id) + const [record] = await tx.select().from(project).where(eq(project.id, id)) + if (!record) continue + const members = await tx + .select({ id: workspace.id, archivedAt: workspace.archivedAt }) + .from(projectWorkspace) + .innerJoin(workspace, eq(workspace.id, projectWorkspace.workspaceId)) + .where(eq(projectWorkspace.projectId, id)) + const survivors = members.filter((row) => !doomed.has(row.id)) + if (!survivors.length) { + if (record.organizationId || record.ownerId !== userId) + throw new OrchestrationError( + 'conflict', + 'Account deletion cannot remove another owner’s Project' + ) + await tx.delete(projectWorkspace).where(eq(projectWorkspace.projectId, id)) + await tx.delete(project).where(eq(project.id, id)) + continue + } + if (!record.archivedAt && survivors.every((row) => row.archivedAt)) + throw new OrchestrationError( + 'conflict', + 'Archive the Project before deleting its last active environment with your account' + ) + if (record.ownerId !== userId) continue + const admins = await tx + .select({ userId: permissions.userId }) + .from(permissions) + .where( + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.permissionType, 'admin'), + ne(permissions.userId, userId), + inArray( + permissions.entityId, + survivors.map((row) => row.id) + ) + ) + ) + .groupBy(permissions.userId) + .having(sql`count(*) = ${survivors.length}`) + .orderBy(asc(permissions.userId)) + .limit(1) + if (!admins[0]) + throw new OrchestrationError( + 'conflict', + 'Give a teammate admin access to every environment before deleting the Project owner’s account' + ) + await tx + .update(project) + .set({ ownerId: admins[0].userId, updatedAt: new Date() }) + .where(eq(project.id, id)) + } +} diff --git a/apps/sim/lib/projects/application/authorization.ts b/apps/sim/lib/projects/application/authorization.ts new file mode 100644 index 00000000000..a0cf6b571a4 --- /dev/null +++ b/apps/sim/lib/projects/application/authorization.ts @@ -0,0 +1,128 @@ +import type { Principal, SessionPrincipal } from '@sim/auth/principal' +import { member, permissions, project, projectWorkspace, workspace } from '@sim/db/schema' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' +import { and, asc, eq, inArray } from 'drizzle-orm' +import { PrincipalKindAuthorizationError } from '@/lib/core/application/workspace-authorization' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbTransaction } from '@/lib/db/types' +import { CAPABILITY_RULES, refuseCapability } from '@/lib/permission-groups/capabilities' +import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks' +import { resolveVerifiedUserAccessControlContext } from '@/lib/permission-groups/resolve.server' +import type { ProjectOperation } from '@/lib/projects/application/operations' +import { lockProject } from '@/lib/projects/membership' + +export function requireProjectPrincipal( + principal: Principal, + operation: ProjectOperation +): asserts principal is SessionPrincipal { + if (principal.kind !== 'session') + throw new PrincipalKindAuthorizationError(principal.kind, operation.id) +} + +/** The complete environment set is loaded server-side; hidden environments never enter the result. */ +export async function authorizeProject( + tx: DbTransaction, + principal: SessionPrincipal, + operation: ProjectOperation, + input: { projectId: string; organizationId?: string; workspaceId?: string } +) { + await lockProject(tx, input.projectId) + const [record] = await tx.select().from(project).where(eq(project.id, input.projectId)).limit(1) + if ( + !record || + (input.organizationId !== undefined && input.organizationId !== record.organizationId) + ) { + throw new OrchestrationError('not_found', 'Project not found') + } + const [orgMember] = record.organizationId + ? await tx + .select({ role: member.role }) + .from(member) + .where( + and(eq(member.userId, principal.userId), eq(member.organizationId, record.organizationId)) + ) + .limit(1) + .for('share') + : [] + const orgAdmin = isOrgAdminRole(orgMember?.role) + const environments = await tx + .select({ + id: workspace.id, + name: workspace.name, + organizationId: workspace.organizationId, + archivedAt: workspace.archivedAt, + parentId: workspace.forkedFromWorkspaceId, + }) + .from(projectWorkspace) + .innerJoin(workspace, eq(workspace.id, projectWorkspace.workspaceId)) + .where(eq(projectWorkspace.projectId, record.id)) + .orderBy(asc(workspace.id)) + if (environments.some((row) => row.organizationId !== record.organizationId)) + throw new OrchestrationError('conflict', 'Project ownership needs reconciliation') + const grants = environments.length + ? await tx + .select({ id: permissions.entityId, permission: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.userId, principal.userId), + inArray( + permissions.entityId, + environments.map((row) => row.id) + ) + ) + ) + .orderBy(asc(permissions.entityId)) + .for('share') + : [] + const grantsById = new Map(grants.map((row) => [row.id, row.permission])) + const rows = environments.map((row) => ({ ...row, permission: grantsById.get(row.id) ?? null })) + if (operation.access === 'issues' && record.organizationId) + await acquirePermissionGroupOrgLock(tx, record.organizationId) + const active = rows.filter((row) => !row.archivedAt) + const visible = active.filter((row) => orgAdmin || row.permission !== null) + const canAdminister = + orgAdmin || (rows.length > 0 && rows.every((row) => row.permission === 'admin')) + if (!visible.length && !(record.archivedAt && canAdminister)) + throw new OrchestrationError('not_found', 'Project not found') + if (input.workspaceId && !visible.some((row) => row.id === input.workspaceId)) + throw new OrchestrationError('not_found', 'Project not found') + if (operation.access === 'admin' && !canAdminister) + throw new OrchestrationError( + 'forbidden', + 'Organization admin or admin access to every environment is required' + ) + let canUseIssues = !record.archivedAt && visible.length > 0 + if (canUseIssues && visible.length < active.length && record.organizationId) { + for (const environment of visible) { + const { config } = await resolveVerifiedUserAccessControlContext( + principal.userId, + environment.id, + record.organizationId, + tx + ) + if (config && CAPABILITY_RULES['project_issues.use'].deniedBy(config, record.id)) { + canUseIssues = false + break + } + } + } + // permission-group-enforced: project_issues.use — Project-wide all-or-nothing gate follows current environment access. + if (operation.access === 'issues' && !canUseIssues) { + if (record.archivedAt) throw new OrchestrationError('conflict', 'Project is archived') + refuseCapability('project_issues.use') + } + const visibleIds = new Set(visible.map((row) => row.id)) + return { + record, + environmentIds: rows.map((row) => row.id), + canAdminister, + canUseIssues, + environments: visible.map((row) => ({ + id: row.id, + name: row.name, + forkedFromWorkspaceId: row.parentId && visibleIds.has(row.parentId) ? row.parentId : null, + })), + } +} diff --git a/apps/sim/lib/projects/application/index.ts b/apps/sim/lib/projects/application/index.ts new file mode 100644 index 00000000000..22b91983164 --- /dev/null +++ b/apps/sim/lib/projects/application/index.ts @@ -0,0 +1,9 @@ +export { projectOperations } from '@/lib/projects/application/operations' +export { + archiveProject, + getProject, + getProjectIssueAccess, + getWorkspaceProject, + listProjects, + renameProject, +} from '@/lib/projects/application/use-cases' diff --git a/apps/sim/lib/projects/application/operations.ts b/apps/sim/lib/projects/application/operations.ts new file mode 100644 index 00000000000..d95eaa5c6e0 --- /dev/null +++ b/apps/sim/lib/projects/application/operations.ts @@ -0,0 +1,52 @@ +import type { ApplicationOperation } from '@/lib/core/application/operation' +import { assertOperationCapability } from '@/lib/core/application/operation' + +export interface ProjectOperation extends ApplicationOperation { + readonly principalKinds: readonly ['session'] + readonly access: 'read' | 'admin' | 'issues' +} + +/** Project operations cannot borrow authority from workspace keys or an arbitrary root. */ +function defineProjectOperation(operation: O): O { + assertOperationCapability(operation) + Object.freeze(operation.principalKinds) + return Object.freeze(operation) +} + +export const projectOperations = { + // permission-group-exempt: navigation exposes only Projects containing accessible environments. + list: defineProjectOperation({ + id: 'projects.list', + principalKinds: ['session'], + access: 'read', + capability: 'none', + }), + // permission-group-exempt: Project metadata does not grant access to its Issues or environments. + get: defineProjectOperation({ + id: 'projects.get', + principalKinds: ['session'], + access: 'read', + capability: 'none', + }), + // permission-group-exempt: Project names are administered by org admins or admins of every environment. + rename: defineProjectOperation({ + id: 'projects.rename', + principalKinds: ['session'], + access: 'admin', + capability: 'none', + }), + // permission-group-exempt: archival revokes execution rather than granting a governed capability. + archive: defineProjectOperation({ + id: 'projects.archive', + principalKinds: ['session'], + access: 'admin', + capability: 'none', + }), + // permission-group-exempt: project_issues.use is parameterized by Project ID and checked after access. + issues: defineProjectOperation({ + id: 'projects.issues.access', + principalKinds: ['session'], + access: 'issues', + capability: 'none', + }), +} as const diff --git a/apps/sim/lib/projects/application/use-cases.ts b/apps/sim/lib/projects/application/use-cases.ts new file mode 100644 index 00000000000..d67a2c52b89 --- /dev/null +++ b/apps/sim/lib/projects/application/use-cases.ts @@ -0,0 +1,204 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { db } from '@sim/db' +import { member, permissions, project, projectWorkspace, workspace } from '@sim/db/schema' +import { and, asc, eq, gt, isNull, sql } from 'drizzle-orm' +import { recordProjectedUseCaseAuditEntries } from '@/lib/core/application/authorized-workspace-use-case' +import type { OperationUseCase } from '@/lib/core/application/operation' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { authorizeProject, requireProjectPrincipal } from '@/lib/projects/application/authorization' +import { projectOperations } from '@/lib/projects/application/operations' +import { archiveProjectInTransaction, finishProjectArchive } from '@/lib/projects/lifecycle' + +interface ProjectInput { + projectId: string + organizationId?: string + workspaceId?: string +} +type ProjectContext = Awaited> +function presentProject(context: ProjectContext) { + return { + ...context.record, + environments: context.environments, + capabilities: { administer: context.canAdminister, issues: context.canUseIssues }, + } +} + +export const getProject: OperationUseCase< + typeof projectOperations.get, + ProjectInput, + { project: ReturnType } +> = { + operation: projectOperations.get, + async execute({ principal, input }) { + requireProjectPrincipal(principal, projectOperations.get) + return db.transaction(async (tx) => ({ + project: presentProject(await authorizeProject(tx, principal, projectOperations.get, input)), + })) + }, +} + +/** Read-only capability probe. Issue mutations call authorizeProject inside their own transaction. */ +export const getProjectIssueAccess: OperationUseCase< + typeof projectOperations.issues, + ProjectInput, + { projectId: string } +> = { + operation: projectOperations.issues, + async execute({ principal, input }) { + requireProjectPrincipal(principal, projectOperations.issues) + return db.transaction(async (tx) => { + const context = await authorizeProject(tx, principal, projectOperations.issues, input) + return { projectId: context.record.id } + }) + }, +} + +export const listProjects: OperationUseCase< + typeof projectOperations.list, + { organizationId?: string; cursor?: string; limit: number }, + { projects: ReturnType[]; nextCursor: string | null } +> = { + operation: projectOperations.list, + async execute({ principal, input }) { + requireProjectPrincipal(principal, projectOperations.list) + if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) + throw new OrchestrationError('validation', 'Limit must be between 1 and 100') + return db.transaction(async (tx) => { + const candidates = await tx + .select({ id: project.id }) + .from(project) + .where( + and( + isNull(project.archivedAt), + input.organizationId ? eq(project.organizationId, input.organizationId) : undefined, + input.cursor ? gt(project.id, input.cursor) : undefined, + sql`exists (select 1 from ${projectWorkspace} pw join ${workspace} w on w.id = pw.workspace_id + where pw.project_id = ${project.id} and w.archived_at is null and ( + exists (select 1 from ${permissions} pe where pe.entity_type = 'workspace' and pe.entity_id = w.id and pe.user_id = ${principal.userId}) + or exists (select 1 from ${member} m where m.organization_id = w.organization_id and m.user_id = ${principal.userId} and m.role in ('owner', 'admin')) + ))` + ) + ) + .orderBy(asc(project.id)) + .limit(input.limit + 1) + const page = candidates.slice(0, input.limit) + const projects = [] + for (const row of page) + projects.push( + presentProject( + await authorizeProject(tx, principal, projectOperations.list, { + projectId: row.id, + organizationId: input.organizationId, + }) + ) + ) + return { + projects, + nextCursor: candidates.length > input.limit ? (page.at(-1)?.id ?? null) : null, + } + }) + }, +} + +export const renameProject: OperationUseCase< + typeof projectOperations.rename, + ProjectInput & { name: string }, + { id: string; name: string } +> = { + operation: projectOperations.rename, + async execute({ principal, input, request }) { + requireProjectPrincipal(principal, projectOperations.rename) + const name = input.name.trim() + if (!name || name.length > 100) + throw new OrchestrationError('validation', 'Project name must contain 1–100 characters') + const result = await db.transaction(async (tx) => { + const context = await authorizeProject(tx, principal, projectOperations.rename, input) + if (context.record.archivedAt) throw new OrchestrationError('conflict', 'Project is archived') + if (context.record.name === name) return { context, changed: false } + await tx + .update(project) + .set({ name, updatedAt: new Date() }) + .where(eq(project.id, context.record.id)) + return { context, changed: true } + }) + if (result.changed) + recordProjectedUseCaseAuditEntries( + projectOperations.rename, + null, + principal, + request, + [ + { + action: AuditAction.PROJECT_UPDATED, + resourceType: AuditResourceType.PROJECT, + resourceId: input.projectId, + resourceName: name, + }, + ], + result.context.record.organizationId ?? undefined + ) + return { id: input.projectId, name } + }, +} + +export const archiveProject: OperationUseCase< + typeof projectOperations.archive, + ProjectInput, + { id: string; archived: boolean } +> = { + operation: projectOperations.archive, + async execute({ principal, input, request }) { + requireProjectPrincipal(principal, projectOperations.archive) + const result = await db.transaction(async (tx) => { + const context = await authorizeProject(tx, principal, projectOperations.archive, input) + const effects = await archiveProjectInTransaction(tx, context.record.id) + return { context, effects } + }) + if (!result.context.record.archivedAt) + recordProjectedUseCaseAuditEntries( + projectOperations.archive, + null, + principal, + request, + [ + { + action: AuditAction.PROJECT_ARCHIVED, + resourceType: AuditResourceType.PROJECT, + resourceId: input.projectId, + resourceName: result.context.record.name, + }, + ], + result.context.record.organizationId ?? undefined + ) + await finishProjectArchive(result.effects, `project:${input.projectId}`) + return { id: input.projectId, archived: true } + }, +} + +/** Resolves membership and authorizes the requested environment before returning Project data. */ +export const getWorkspaceProject: OperationUseCase< + typeof projectOperations.get, + { workspaceId: string }, + { project: ReturnType } +> = { + operation: projectOperations.get, + async execute({ principal, input }) { + requireProjectPrincipal(principal, projectOperations.get) + return db.transaction(async (tx) => { + const [membership] = await tx + .select({ projectId: projectWorkspace.projectId }) + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, input.workspaceId)) + .limit(1) + if (!membership) throw new OrchestrationError('not_found', 'Project not found') + return { + project: presentProject( + await authorizeProject(tx, principal, projectOperations.get, { + projectId: membership.projectId, + workspaceId: input.workspaceId, + }) + ), + } + }) + }, +} diff --git a/apps/sim/lib/projects/lifecycle.ts b/apps/sim/lib/projects/lifecycle.ts new file mode 100644 index 00000000000..d089f7c2e38 --- /dev/null +++ b/apps/sim/lib/projects/lifecycle.ts @@ -0,0 +1,66 @@ +import { + project, + projectWorkspace, + workflow, + workflowMcpServer, + workflowMcpTool, + workspace, +} from '@sim/db/schema' +import { and, asc, eq, isNull } from 'drizzle-orm' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbTransaction } from '@/lib/db/types' +import { lockProject } from '@/lib/projects/membership' +import { archiveWorkflowInTransaction, finishWorkflowArchive } from '@/lib/workflows/lifecycle' +import { archiveWorkspaceInTransaction, finishWorkspaceArchive } from '@/lib/workspaces/lifecycle' + +/** All durable archive state commits together; external notifications follow the commit. */ +export async function archiveProjectInTransaction(tx: DbTransaction, projectId: string) { + await lockProject(tx, projectId) + const [record] = await tx.select().from(project).where(eq(project.id, projectId)) + if (!record) throw new OrchestrationError('not_found', 'Project not found') + const now = record.archivedAt ?? new Date() + const members = await tx + .select({ id: projectWorkspace.workspaceId }) + .from(projectWorkspace) + .where(eq(projectWorkspace.projectId, projectId)) + .orderBy(asc(projectWorkspace.workspaceId)) + const workflows: { id: string; workspaceId: string; serverIds: string[] }[] = [] + const environments: { id: string; serverIds: string[] }[] = [] + for (const { id: workspaceId } of members) { + await tx + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .for('update') + const rows = await tx + .select({ id: workflow.id }) + .from(workflow) + .where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt))) + .orderBy(asc(workflow.id)) + for (const row of rows) { + const servers = await tx + .select({ id: workflowMcpTool.serverId }) + .from(workflowMcpTool) + .where(eq(workflowMcpTool.workflowId, row.id)) + await archiveWorkflowInTransaction(tx, row.id, now) + workflows.push({ id: row.id, workspaceId, serverIds: servers.map((server) => server.id) }) + } + const servers = await tx + .select({ id: workflowMcpServer.id }) + .from(workflowMcpServer) + .where(eq(workflowMcpServer.workspaceId, workspaceId)) + await archiveWorkspaceInTransaction(tx, workspaceId, now) + environments.push({ id: workspaceId, serverIds: servers.map((server) => server.id) }) + } + await tx.update(project).set({ archivedAt: now, updatedAt: now }).where(eq(project.id, projectId)) + return { workflows, environments } +} + +export async function finishProjectArchive( + effects: Awaited>, + requestId: string +): Promise { + for (const row of effects.workflows) + await finishWorkflowArchive(row.id, row.workspaceId, row.serverIds, { requestId }) + for (const row of effects.environments) await finishWorkspaceArchive(row.id, row.serverIds) +} diff --git a/apps/sim/lib/projects/membership.ts b/apps/sim/lib/projects/membership.ts new file mode 100644 index 00000000000..7a328d71702 --- /dev/null +++ b/apps/sim/lib/projects/membership.ts @@ -0,0 +1,238 @@ +import { permissionGroup, project, projectWorkspace, workspace } from '@sim/db/schema' +import { getPostgresErrorCode } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, inArray, isNull, ne, notInArray, sql } from 'drizzle-orm' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbOrTx, DbTransaction } from '@/lib/db/types' + +/** Canonical Project mutex; membership and lifecycle writers hold it until commit. */ +export async function lockProject(tx: DbTransaction, projectId: string): Promise { + await tx.execute(sql`SELECT set_config('lock_timeout', '5000ms', true)`) + try { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`project:${projectId}`}, 0))` + ) + } catch (error) { + if (getPostgresErrorCode(error) === '55P03') + throw new OrchestrationError('conflict', 'Project is changing; retry the operation') + throw error + } +} + +function generatedProjectName(workspaceName: string): string { + const suffix = ' - Project' + return `${(workspaceName.trim() || 'Untitled').slice(0, 100 - suffix.length)}${suffix}` +} + +export async function createProjectForWorkspace( + tx: DbTransaction, + input: { + workspaceId: string + name: string + organizationId: string | null + ownerId: string + archivedAt?: Date | null + } +): Promise { + const id = generateId() + await tx.insert(project).values({ + id, + name: generatedProjectName(input.name), + organizationId: input.organizationId, + ownerId: input.ownerId, + archivedAt: input.archivedAt ?? null, + }) + await tx.insert(projectWorkspace).values({ projectId: id, workspaceId: input.workspaceId }) + return id +} + +/** Returns null only for a legacy workspace awaiting the operator-run backfill. */ +export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: string) { + const [membership] = await tx + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, workspaceId)) + .limit(1) + if (!membership) return null + await lockProject(tx, membership.projectId) + const [current] = await tx + .select({ project }) + .from(projectWorkspace) + .innerJoin(project, eq(project.id, projectWorkspace.projectId)) + .where(eq(projectWorkspace.workspaceId, workspaceId)) + .limit(1) + if (!current || current.project.id !== membership.projectId) { + throw new OrchestrationError('conflict', 'Project membership changed; retry the operation') + } + return current.project +} + +export async function requireForkProject(tx: DbTransaction, parentWorkspaceId: string) { + const parent = await lockWorkspaceProject(tx, parentWorkspaceId) + if (!parent) + throw new OrchestrationError( + 'conflict', + 'This workspace needs the Project backfill before it can be forked' + ) + if (parent.archivedAt) throw new OrchestrationError('conflict', 'Cannot fork an archived Project') + return parent +} + +/** Individual removal cannot leave an active Project without an active environment. */ +export async function requireRemainingProjectEnvironment( + tx: DbTransaction, + workspaceId: string +): Promise { + const owner = await lockWorkspaceProject(tx, workspaceId) + if (!owner) return + const [remaining] = await tx + .select({ id: workspace.id }) + .from(projectWorkspace) + .innerJoin(workspace, eq(workspace.id, projectWorkspace.workspaceId)) + .where( + and( + eq(projectWorkspace.projectId, owner.id), + ne(workspace.id, workspaceId), + isNull(workspace.archivedAt) + ) + ) + .limit(1) + if (!remaining && !owner.archivedAt) { + throw new OrchestrationError( + 'conflict', + 'The last active environment cannot be removed. Archive the Project instead.' + ) + } +} + +/** Called before clearing the edge, under the existing lineage lock. */ +export async function splitForkProject( + tx: DbTransaction, + workspaceId: string +): Promise { + const owner = await lockWorkspaceProject(tx, workspaceId) + if (!owner) return null + if (owner.archivedAt) + throw new OrchestrationError('conflict', 'Cannot disconnect an archived Project') + const rows = await tx.execute<{ + id: string + name: string + owner_id: string + archived_at: Date | null + project_id: string | null + }>(sql` + WITH RECURSIVE descendants AS ( + SELECT id, name, owner_id, archived_at FROM workspace WHERE id = ${workspaceId} + UNION + SELECT w.id, w.name, w.owner_id, w.archived_at FROM workspace w JOIN descendants d ON w.forked_from_workspace_id = d.id + ) SELECT d.*, pw.project_id FROM descendants d LEFT JOIN project_workspace pw ON pw.workspace_id = d.id + `) + if (rows.some((row) => row.project_id !== owner.id)) + throw new OrchestrationError( + 'conflict', + 'Fork descendants need Project membership reconciliation before disconnecting' + ) + const root = rows.find((row) => row.id === workspaceId) + if (!root || rows.every((row) => row.archived_at)) + throw new OrchestrationError('conflict', 'A new Project needs an active environment') + const ids = rows.map((row) => row.id) + const [remaining] = await tx + .select({ id: workspace.id }) + .from(projectWorkspace) + .innerJoin(workspace, eq(workspace.id, projectWorkspace.workspaceId)) + .where( + and( + eq(projectWorkspace.projectId, owner.id), + isNull(workspace.archivedAt), + notInArray(workspace.id, ids) + ) + ) + .limit(1) + if (!remaining) + throw new OrchestrationError( + 'conflict', + 'Disconnecting would remove the last active environment from this Project' + ) + const id = generateId() + await tx.insert(project).values({ + id, + name: generatedProjectName(root.name), + organizationId: owner.organizationId, + ownerId: root.owner_id, + }) + await tx + .update(projectWorkspace) + .set({ projectId: id }) + .where( + and(eq(projectWorkspace.projectId, owner.id), inArray(projectWorkspace.workspaceId, ids)) + ) + if (owner.organizationId) { + await tx.execute(sql` + UPDATE ${permissionGroup} + SET config = jsonb_set(config, '{deniedPartialAccessProjectIssues}', + (config->'deniedPartialAccessProjectIssues') || to_jsonb(${id}::text)), updated_at = now() + WHERE organization_id = ${owner.organizationId} + AND config->'deniedPartialAccessProjectIssues' ? ${owner.id} + `) + } + return id +} + +/** Ownership changes include the complete Project; never silently split Project-wide resources. */ +export async function transferWorkspaceProjects( + tx: DbTransaction, + workspaceIds: string[], + organizationId: string | null, + ownerId?: string +): Promise { + if (!workspaceIds.length) return + const owners = await tx + .selectDistinct({ id: projectWorkspace.projectId }) + .from(projectWorkspace) + .where(inArray(projectWorkspace.workspaceId, workspaceIds)) + .orderBy(asc(projectWorkspace.projectId)) + const selected = new Set(workspaceIds) + for (const owner of owners) { + await tryLockProject(tx, owner.id) + const members = await tx + .select({ id: projectWorkspace.workspaceId }) + .from(projectWorkspace) + .where(eq(projectWorkspace.projectId, owner.id)) + if (members.some((row) => !selected.has(row.id))) { + throw new OrchestrationError( + 'conflict', + 'Move all environments in the Project together, or disconnect the fork first' + ) + } + const [current] = await tx + .select({ organizationId: project.organizationId }) + .from(project) + .where(eq(project.id, owner.id)) + if (current?.organizationId && current.organizationId !== organizationId) { + await tx.execute(sql` + UPDATE ${permissionGroup} + SET config = jsonb_set(config, '{deniedPartialAccessProjectIssues}', + (config->'deniedPartialAccessProjectIssues') - ${owner.id}), updated_at = now() + WHERE organization_id = ${current.organizationId} + AND config->'deniedPartialAccessProjectIssues' ? ${owner.id} + `) + } + await tx + .update(project) + .set({ + organizationId, + ownerId, + updatedAt: new Date(), + }) + .where(eq(project.id, owner.id)) + } +} + +/** Existing ownership paths can hold workspace rows first; refuse contention instead of inverting locks. */ +export async function tryLockProject(tx: DbOrTx, projectId: string): Promise { + const [lock] = await tx.execute<{ acquired: boolean }>( + sql`SELECT pg_try_advisory_xact_lock(hashtextextended(${`project:${projectId}`}, 0)) AS acquired` + ) + if (!lock?.acquired) + throw new OrchestrationError('conflict', 'Project is changing; retry the ownership change') +} diff --git a/apps/sim/lib/users/account-deletion.ts b/apps/sim/lib/users/account-deletion.ts index b9876209500..57f41ff7e79 100644 --- a/apps/sim/lib/users/account-deletion.ts +++ b/apps/sim/lib/users/account-deletion.ts @@ -25,6 +25,7 @@ import type { import { getHighestPriorityPersonalSubscription } from '@/lib/billing/core/plan' import { isSoleOwnerOfPaidOrganization } from '@/lib/billing/organizations/membership' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion' import { appendTableEvent, type TableEvent } from '@/lib/table/events' import { type CancelledCellMarker, @@ -681,6 +682,7 @@ export async function deleteUserAccount(userId: string): Promise { + await prepareProjectsForAccountDeletion(tx, userId, doomedWorkspaceIds) if (doomedWorkspaceIds.length > 0) { /** * Re-checked here rather than trusted from the plan: a workspace that diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index 965ae9ca62b..27e59772f40 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -3,6 +3,7 @@ import { apiKey, chat, folder as folderTable, + projectWorkspace, webhook, workflow, workflowDeploymentVersion, @@ -16,6 +17,7 @@ import { env } from '@/lib/core/config/env' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' import { getSocketServerUrl } from '@/lib/core/utils/urls' +import type { DbTransaction } from '@/lib/db/types' import { mcpPubSub } from '@/lib/mcp/pubsub' import { releaseWebhookPathClaims } from '@/lib/webhooks/path-claims' import { supersedeInFlightDeploymentOperations } from '@/lib/workflows/persistence/deployment-operations' @@ -106,87 +108,14 @@ export async function archiveWorkflow( .from(workflowMcpTool) .where(and(eq(workflowMcpTool.workflowId, workflowId), isNull(workflowMcpTool.archivedAt))) - await db.transaction(async (tx) => { - await supersedeInFlightDeploymentOperations(tx, workflowId) - await releaseWebhookPathClaims(tx, workflowId) - - await tx - .update(workflowSchedule) - .set({ - archivedAt: now, - updatedAt: now, - status: 'disabled', - nextRunAt: null, - lastQueuedAt: null, - }) - .where(and(eq(workflowSchedule.workflowId, workflowId), isNull(workflowSchedule.archivedAt))) - - await tx - .update(webhook) - .set({ - archivedAt: now, - updatedAt: now, - isActive: false, - }) - .where(and(eq(webhook.workflowId, workflowId), isNull(webhook.archivedAt))) - - await tx - .update(chat) - .set({ - archivedAt: now, - updatedAt: now, - isActive: false, - }) - .where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt))) - - await tx - .update(workflowMcpTool) - .set({ - archivedAt: now, - updatedAt: now, - }) - .where(and(eq(workflowMcpTool.workflowId, workflowId), isNull(workflowMcpTool.archivedAt))) - - await tx - .update(workflowDeploymentVersion) - .set({ - isActive: false, - }) - .where(eq(workflowDeploymentVersion.workflowId, workflowId)) - - await tx - .update(workflow) - .set({ - archivedAt: now, - updatedAt: now, - isDeployed: false, - isPublicApi: false, - }) - .where(and(eq(workflow.id, workflowId), isNull(workflow.archivedAt))) - }) - - try { - PlatformEvents.workflowDeleted({ - workflowId, - workspaceId: existingWorkflow.workspaceId || undefined, - }) - } catch {} - - if (options.notifySocket !== false) { - await notifyWorkflowArchived(workflowId, options.requestId) - } - - await cleanupExternalWebhooksForWorkflow(workflowId, options.requestId) + await db.transaction((tx) => archiveWorkflowInTransaction(tx, workflowId, now)) - if (existingWorkflow.workspaceId && mcpPubSub && affectedWorkflowMcpServers.length > 0) { - const uniqueServerIds = [...new Set(affectedWorkflowMcpServers.map((row) => row.serverId))] - for (const serverId of uniqueServerIds) { - mcpPubSub.publishWorkflowToolsChanged({ - serverId, - workspaceId: existingWorkflow.workspaceId, - }) - } - } + await finishWorkflowArchive( + workflowId, + existingWorkflow.workspaceId, + affectedWorkflowMcpServers.map((row) => row.serverId), + options + ) return { archived: true, @@ -368,12 +297,131 @@ export async function disableUserResources(userId: string): Promise { .from(workspace) .where(and(eq(workspace.ownerId, userId), isNull(workspace.archivedAt))) - await Promise.all([ - ...ownedWorkspaces.map((w) => archiveWorkspace(w.id, { requestId })), - db.delete(apiKey).where(eq(apiKey.userId, userId)), - ]) + const { archiveProjectInTransaction, finishProjectArchive } = await import( + '@/lib/projects/lifecycle' + ) + const { lockWorkspaceProject } = await import('@/lib/projects/membership') + const owned = new Set(ownedWorkspaces.map((row) => row.id)) + const processed = new Set() + for (const row of ownedWorkspaces) { + if (processed.has(row.id)) continue + const archived = await db.transaction(async (tx) => { + const record = await lockWorkspaceProject(tx, row.id) + if (!record) return null + const active = await tx + .select({ id: workspace.id }) + .from(projectWorkspace) + .innerJoin(workspace, eq(workspace.id, projectWorkspace.workspaceId)) + .where(and(eq(projectWorkspace.projectId, record.id), isNull(workspace.archivedAt))) + if (!active.every((entry) => owned.has(entry.id))) return null + return archiveProjectInTransaction(tx, record.id) + }) + if (archived) { + for (const entry of archived.environments) processed.add(entry.id) + await finishProjectArchive(archived, requestId) + } else { + await archiveWorkspace(row.id, { requestId }) + processed.add(row.id) + } + } + await db.delete(apiKey).where(eq(apiKey.userId, userId)) logger.info( `[${requestId}] Disabled resources for user ${userId}: archived ${ownedWorkspaces.length} workspaces, deleted API keys` ) } + +/** Durable archive state shared by single-workflow and compound Project archival. */ +export async function archiveWorkflowInTransaction( + tx: DbTransaction, + workflowId: string, + now: Date +): Promise { + await supersedeInFlightDeploymentOperations(tx, workflowId) + await releaseWebhookPathClaims(tx, workflowId) + + await tx + .update(workflowSchedule) + .set({ + archivedAt: now, + updatedAt: now, + status: 'disabled', + nextRunAt: null, + lastQueuedAt: null, + }) + .where(and(eq(workflowSchedule.workflowId, workflowId), isNull(workflowSchedule.archivedAt))) + + await tx + .update(webhook) + .set({ + archivedAt: now, + updatedAt: now, + isActive: false, + }) + .where(and(eq(webhook.workflowId, workflowId), isNull(webhook.archivedAt))) + + await tx + .update(chat) + .set({ + archivedAt: now, + updatedAt: now, + isActive: false, + }) + .where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt))) + + await tx + .update(workflowMcpTool) + .set({ + archivedAt: now, + updatedAt: now, + }) + .where(and(eq(workflowMcpTool.workflowId, workflowId), isNull(workflowMcpTool.archivedAt))) + + await tx + .update(workflowDeploymentVersion) + .set({ + isActive: false, + }) + .where(eq(workflowDeploymentVersion.workflowId, workflowId)) + + await tx + .update(workflow) + .set({ + archivedAt: now, + updatedAt: now, + isDeployed: false, + isPublicApi: false, + }) + .where(and(eq(workflow.id, workflowId), isNull(workflow.archivedAt))) +} + +/** Best-effort external notifications run only after durable archive state commits. */ +export async function finishWorkflowArchive( + workflowId: string, + workspaceId: string | null, + serverIds: string[], + options: ArchiveWorkflowOptions +): Promise { + try { + PlatformEvents.workflowDeleted({ + workflowId, + workspaceId: workspaceId || undefined, + }) + } catch {} + + if (options.notifySocket !== false) { + await notifyWorkflowArchived(workflowId, options.requestId) + } + + await cleanupExternalWebhooksForWorkflow(workflowId, options.requestId) + + if (workspaceId && mcpPubSub && serverIds.length > 0) { + const uniqueServerIds = [...new Set(serverIds)] + for (const serverId of uniqueServerIds) { + mcpPubSub.publishWorkflowToolsChanged({ + serverId, + workspaceId: workspaceId, + }) + } + } +} diff --git a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.test.ts b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.test.ts index b8eef76dee3..c5ed40c2550 100644 --- a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.test.ts +++ b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.test.ts @@ -2,7 +2,9 @@ import { auditMock, dbChainMockFns, posthogServerMock, + queueTableRows, resetDbChainMock, + schemaMock, workflowAuthzMockFns, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, @@ -45,6 +47,9 @@ const createParams = { describe('performCreateWorkflowTransition unique-violation handling', () => { beforeEach(() => { resetDbChainMock() + queueTableRows(schemaMock.workspace, [ + { archivedAt: null, forkSyncNewWorkflowsExcluded: false }, + ]) workflowAuthzMockFns.mockIsFolderInWorkspace.mockResolvedValue(true) workflowsPersistenceUtilsMockFns.mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true, diff --git a/apps/sim/lib/workflows/persistence/duplicate.test.ts b/apps/sim/lib/workflows/persistence/duplicate.test.ts index 7a6706e9b46..d9a2275bf50 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.test.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.test.ts @@ -51,6 +51,9 @@ function insertedValuesFor(table: unknown): unknown[] { describe('duplicateWorkflow ordering', () => { beforeEach(() => { resetDbChainMock() + queueTableRows(schemaMock.workspace, [ + { archivedAt: null, forkSyncNewWorkflowsExcluded: false }, + ]) vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('new-workflow-id'), diff --git a/apps/sim/lib/workflows/persistence/new-workflow-row.ts b/apps/sim/lib/workflows/persistence/new-workflow-row.ts index e1e10809137..36091a228f8 100644 --- a/apps/sim/lib/workflows/persistence/new-workflow-row.ts +++ b/apps/sim/lib/workflows/persistence/new-workflow-row.ts @@ -1,6 +1,7 @@ import { type workflow, workspace } from '@sim/db/schema' import { and, eq, isNull } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' +import { lockActiveWorkspace } from '@/lib/workspaces/active-workspace' interface NewWorkflowRowInput { id: string @@ -40,6 +41,7 @@ export async function readForkSyncNewWorkflowsExcluded( * `copyWorkflowStateIntoTarget` instead. */ export async function buildNewWorkflowRow(executor: DbOrTx, input: NewWorkflowRowInput) { + const workspace = await lockActiveWorkspace(executor, input.workspaceId) const now = input.now ?? new Date() return { id: input.id, @@ -55,6 +57,6 @@ export async function buildNewWorkflowRow(executor: DbOrTx, input: NewWorkflowRo isDeployed: false, runCount: 0, variables: input.variables ?? {}, - forkSyncExcluded: await readForkSyncNewWorkflowsExcluded(executor, input.workspaceId), + forkSyncExcluded: workspace.forkSyncNewWorkflowsExcluded, } satisfies typeof workflow.$inferInsert } diff --git a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts index bfd53dbe599..1b30b3d4a3f 100644 --- a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts @@ -8,6 +8,8 @@ import { knowledgeBase, outboxEvent, permissions, + project, + projectWorkspace, user, userTableDefinitions, workflow, @@ -25,6 +27,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' import { processOutboxEventById } from '@/lib/core/outbox/service' import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' +import { createProjectForWorkspace } from '@/lib/projects/membership' import { workflowDeploymentOutboxHandlers } from '@/lib/workflows/deployment-outbox' import { performCreateWorkflowTransition } from '@/lib/workflows/orchestration/workflow-lifecycle' import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate' @@ -137,6 +140,14 @@ describe('authorized fork and sync against PostgreSQL', () => { billedAccountUserId: userId, allowPersonalApiKeys: true, }) + await db.transaction((tx) => + createProjectForWorkspace(tx, { + workspaceId: sourceWorkspaceId, + name: 'Fork source fixture', + ownerId: userId, + organizationId: null, + }) + ) await db.insert(permissions).values({ id: generateId(), userId, @@ -169,6 +180,19 @@ describe('authorized fork and sync against PostgreSQL', () => { }) }) afterAll(async () => { + const owned = await db + .select({ id: project.id }) + .from(project) + .where(eq(project.ownerId, userId)) + if (owned.length) { + await db.delete(projectWorkspace).where( + inArray( + projectWorkspace.projectId, + owned.map((row) => row.id) + ) + ) + await db.delete(project).where(eq(project.ownerId, userId)) + } for (const id of createdWorkspaceIds) await db.delete(workspace).where(eq(workspace.id, id)) await db.delete(workspace).where(eq(workspace.id, sourceWorkspaceId)) await db.delete(user).where(eq(user.id, userId)) diff --git a/apps/sim/lib/workspaces/active-workspace.ts b/apps/sim/lib/workspaces/active-workspace.ts new file mode 100644 index 00000000000..f506aa02ae8 --- /dev/null +++ b/apps/sim/lib/workspaces/active-workspace.ts @@ -0,0 +1,19 @@ +import { workspace } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbOrTx } from '@/lib/db/types' + +/** Transactional resource creation holds this row through insertion so archival cannot overtake it. */ +export async function lockActiveWorkspace(executor: DbOrTx, workspaceId: string) { + const [record] = await executor + .select({ + archivedAt: workspace.archivedAt, + forkSyncNewWorkflowsExcluded: workspace.forkSyncNewWorkflowsExcluded, + }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .for('share') + .limit(1) + if (!record || record.archivedAt) throw new OrchestrationError('not_found', 'Workspace not found') + return record +} diff --git a/apps/sim/lib/workspaces/admin-move.ts b/apps/sim/lib/workspaces/admin-move.ts index 1571faf8af3..662ef5807ec 100644 --- a/apps/sim/lib/workspaces/admin-move.ts +++ b/apps/sim/lib/workspaces/admin-move.ts @@ -41,6 +41,7 @@ import type { DbOrTx } from '@/lib/db/types' import { getInvitationById, isInvitationExpired } from '@/lib/invitations/core' import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' import { PENDING_INVITATION_UNIQUE_INDEX, sendInvitationEmail } from '@/lib/invitations/send' +import { transferWorkspaceProjects } from '@/lib/projects/membership' import { invalidateWorkspaceTableLimitsCache } from '@/lib/table/billing' import { deleteCustomBlock } from '@/lib/workflows/custom-blocks/operations' import { @@ -1356,6 +1357,8 @@ export async function moveWorkspaceToOrganization(params: { }) : { detachedPermissionGroupIds: [] } + await transferWorkspaceProjects(tx, [params.workspaceId], params.destinationOrganizationId) + await changeWorkspaceStoragePayerInTx(tx, { workspaceId: params.workspaceId, organizationId: params.destinationOrganizationId, diff --git a/apps/sim/lib/workspaces/create.test.ts b/apps/sim/lib/workspaces/create.test.ts index fc42089f009..680a6fd1883 100644 --- a/apps/sim/lib/workspaces/create.test.ts +++ b/apps/sim/lib/workspaces/create.test.ts @@ -1,6 +1,8 @@ +import { workspace } from '@sim/db/schema' import { dbChainMock, dbChainMockFns, + queueTableRows, resetDbChainMock, workflowsPersistenceUtilsMock, } from '@sim/testing' @@ -122,6 +124,7 @@ describe('createDefaultPersonalWorkspaceInTransaction', () => { */ it('creates an ungoverned personal workspace and resolves no regime', async () => { mockLockWorkspaceCreationContext.mockResolvedValue({ billedAccountUserId: 'user-1' }) + queueTableRows(workspace, [{ archivedAt: null, forkSyncNewWorkflowsExcluded: false }]) const tx = dbChainMock.db as unknown as DbOrTx await createDefaultPersonalWorkspaceInTransaction(tx, { diff --git a/apps/sim/lib/workspaces/create.ts b/apps/sim/lib/workspaces/create.ts index 8abfb8d2c99..b8aea562d5a 100644 --- a/apps/sim/lib/workspaces/create.ts +++ b/apps/sim/lib/workspaces/create.ts @@ -5,6 +5,7 @@ import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/erro import { generateId } from '@sim/utils/id' import { PlatformEvents } from '@/lib/core/telemetry' import type { DbTransaction } from '@/lib/db/types' +import { createProjectForWorkspace } from '@/lib/projects/membership' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -129,6 +130,13 @@ export async function createWorkspaceInTransaction( updatedAt: now, }) + await createProjectForWorkspace(tx, { + workspaceId, + name, + organizationId: organizationId ?? null, + ownerId: userId, + }) + const permissionRows = [ { id: generateId(), diff --git a/apps/sim/lib/workspaces/lifecycle.test.ts b/apps/sim/lib/workspaces/lifecycle.test.ts deleted file mode 100644 index 5d447ae689b..00000000000 --- a/apps/sim/lib/workspaces/lifecycle.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - dbChainMockFns, - permissionsMock, - permissionsMockFns, - queueTableRows, - resetDbChainMock, - schemaMock, -} from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockArchiveWorkflowsForWorkspace } = vi.hoisted(() => ({ - mockArchiveWorkflowsForWorkspace: vi.fn(), -})) - -const mockGetWorkspaceWithOwner = permissionsMockFns.mockGetWorkspaceWithOwner - -vi.mock('@/lib/workflows/lifecycle', () => ({ - archiveWorkflowsForWorkspace: (...args: unknown[]) => mockArchiveWorkflowsForWorkspace(...args), -})) - -vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) - -import { archiveWorkspace } from '@/lib/workspaces/lifecycle' - -function createUpdateChain() { - return { - set: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([]), - }), - } -} - -describe('workspace lifecycle', () => { - beforeEach(() => { - resetDbChainMock() - }) - - afterAll(() => { - resetDbChainMock() - }) - - it('archives workspace and dependent resources', async () => { - mockGetWorkspaceWithOwner.mockResolvedValue({ - id: 'workspace-1', - name: 'Workspace 1', - ownerId: 'user-1', - archivedAt: null, - }) - mockArchiveWorkflowsForWorkspace.mockResolvedValue(2) - queueTableRows(schemaMock.workflowMcpServer, [{ id: 'server-1' }]) - - const tx = { - select: vi.fn().mockReturnValue({ - from: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([{ id: 'kb-1' }]), - }), - }), - update: vi.fn().mockImplementation(() => createUpdateChain()), - delete: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - } - dbChainMockFns.transaction.mockImplementation( - async (callback: (trx: typeof tx) => Promise) => callback(tx) - ) - - const result = await archiveWorkspace('workspace-1', { requestId: 'req-1' }) - - expect(result).toEqual({ - archived: true, - workspaceName: 'Workspace 1', - }) - expect(mockArchiveWorkflowsForWorkspace).toHaveBeenCalledWith('workspace-1', { - requestId: 'req-1', - }) - expect(tx.update).toHaveBeenCalledTimes(9) - expect(tx.delete).toHaveBeenCalledTimes(1) - }) - - it('retries child cleanup for already archived workspaces', async () => { - mockGetWorkspaceWithOwner.mockResolvedValue({ - id: 'workspace-1', - name: 'Workspace 1', - ownerId: 'user-1', - archivedAt: new Date(), - }) - - const result = await archiveWorkspace('workspace-1', { requestId: 'req-1' }) - - expect(result).toEqual({ - archived: false, - workspaceName: 'Workspace 1', - }) - expect(mockArchiveWorkflowsForWorkspace).toHaveBeenCalledWith('workspace-1', { - requestId: 'req-1', - }) - expect(dbChainMockFns.transaction).toHaveBeenCalledOnce() - }) -}) diff --git a/apps/sim/lib/workspaces/lifecycle.ts b/apps/sim/lib/workspaces/lifecycle.ts index e66f5465644..078af37507d 100644 --- a/apps/sim/lib/workspaces/lifecycle.ts +++ b/apps/sim/lib/workspaces/lifecycle.ts @@ -14,8 +14,10 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' +import type { DbTransaction } from '@/lib/db/types' import { mcpPubSub } from '@/lib/mcp/pubsub' import { mcpService } from '@/lib/mcp/service' +import { requireRemainingProjectEnvironment } from '@/lib/projects/membership' import { archiveWorkflowsForWorkspace } from '@/lib/workflows/lifecycle' import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' @@ -43,129 +45,145 @@ export async function archiveWorkspace( .where(eq(workflowMcpServer.workspaceId, workspaceId)) await db.transaction(async (tx) => { - await tx - .update(knowledgeBase) - .set({ - deletedAt: now, - updatedAt: now, - }) - .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))) - - const workspaceKbIds = await tx - .select({ id: knowledgeBase.id }) - .from(knowledgeBase) - .where(eq(knowledgeBase.workspaceId, workspaceId)) - - const knowledgeBaseIds = workspaceKbIds.map((entry) => entry.id) - if (knowledgeBaseIds.length > 0) { - await tx - .update(document) - .set({ archivedAt: now }) - .where( - and( - inArray(document.knowledgeBaseId, knowledgeBaseIds), - isNull(document.archivedAt), - isNull(document.deletedAt) - ) - ) + await requireRemainingProjectEnvironment(tx, workspaceId) + await archiveWorkspaceInTransaction(tx, workspaceId, now) + }) - await tx - .update(knowledgeConnector) - .set({ archivedAt: now, status: 'paused', updatedAt: now }) - .where( - and( - inArray(knowledgeConnector.knowledgeBaseId, knowledgeBaseIds), - isNull(knowledgeConnector.archivedAt), - isNull(knowledgeConnector.deletedAt) - ) - ) - } + await archiveWorkflowsForWorkspace(workspaceId, options) + + logger.info(`[${options.requestId}] Archived workspace ${workspaceId}`) + + await finishWorkspaceArchive( + workspaceId, + workflowMcpServerIds.map((server) => server.id) + ) + + return { + archived: !workspaceRecord.archivedAt, + workspaceName: workspaceRecord.name, + } +} +/** Durable environment archive changes; callers own Project minimum-environment checks. */ +export async function archiveWorkspaceInTransaction( + tx: DbTransaction, + workspaceId: string, + now: Date +): Promise { + await tx + .update(knowledgeBase) + .set({ + deletedAt: now, + updatedAt: now, + }) + .where(and(eq(knowledgeBase.workspaceId, workspaceId), isNull(knowledgeBase.deletedAt))) + + const workspaceKbIds = await tx + .select({ id: knowledgeBase.id }) + .from(knowledgeBase) + .where(eq(knowledgeBase.workspaceId, workspaceId)) + + const knowledgeBaseIds = workspaceKbIds.map((entry) => entry.id) + if (knowledgeBaseIds.length > 0) { await tx - .update(userTableDefinitions) - .set({ - archivedAt: now, - updatedAt: now, - }) + .update(document) + .set({ archivedAt: now }) .where( and( - eq(userTableDefinitions.workspaceId, workspaceId), - isNull(userTableDefinitions.archivedAt) + inArray(document.knowledgeBaseId, knowledgeBaseIds), + isNull(document.archivedAt), + isNull(document.deletedAt) ) ) await tx - .update(workspaceFiles) - .set({ - deletedAt: now, - }) - .where(and(eq(workspaceFiles.workspaceId, workspaceId), isNull(workspaceFiles.deletedAt))) - - await tx - .update(invitation) - .set({ - status: 'cancelled', - updatedAt: now, - }) + .update(knowledgeConnector) + .set({ archivedAt: now, status: 'paused', updatedAt: now }) .where( and( - eq(invitation.status, 'pending'), - sql`${invitation.id} IN ( + inArray(knowledgeConnector.knowledgeBaseId, knowledgeBaseIds), + isNull(knowledgeConnector.archivedAt), + isNull(knowledgeConnector.deletedAt) + ) + ) + } + + await tx + .update(userTableDefinitions) + .set({ + archivedAt: now, + updatedAt: now, + }) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + + await tx + .update(workspaceFiles) + .set({ + deletedAt: now, + }) + .where(and(eq(workspaceFiles.workspaceId, workspaceId), isNull(workspaceFiles.deletedAt))) + + await tx + .update(invitation) + .set({ + status: 'cancelled', + updatedAt: now, + }) + .where( + and( + eq(invitation.status, 'pending'), + sql`${invitation.id} IN ( SELECT ${invitationWorkspaceGrant.invitationId} FROM ${invitationWorkspaceGrant} WHERE ${invitationWorkspaceGrant.workspaceId} = ${workspaceId} )` - ) ) + ) + + await tx + .delete(apiKey) + .where(and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.type, 'workspace'))) + + await tx + .update(workflowMcpServer) + .set({ + deletedAt: now, + isPublic: false, + updatedAt: now, + }) + .where(eq(workflowMcpServer.workspaceId, workspaceId)) - await tx - .delete(apiKey) - .where(and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.type, 'workspace'))) - - await tx - .update(workflowMcpServer) - .set({ - deletedAt: now, - isPublic: false, - updatedAt: now, - }) - .where(eq(workflowMcpServer.workspaceId, workspaceId)) - - await tx - .update(mcpServers) - .set({ - deletedAt: now, - enabled: false, - updatedAt: now, - }) - .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) - - await tx - .update(workspace) - .set({ - archivedAt: now, - updatedAt: now, - }) - .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt))) - }) - - await archiveWorkflowsForWorkspace(workspaceId, options) - - logger.info(`[${options.requestId}] Archived workspace ${workspaceId}`) + await tx + .update(mcpServers) + .set({ + deletedAt: now, + enabled: false, + updatedAt: now, + }) + .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) + + await tx + .update(workspace) + .set({ + archivedAt: now, + updatedAt: now, + }) + .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt))) +} +/** Refreshes derived MCP state after the archive transaction commits. */ +export async function finishWorkspaceArchive( + workspaceId: string, + serverIds: string[] +): Promise { await mcpService.clearCache(workspaceId).catch(() => undefined) - - if (mcpPubSub && workflowMcpServerIds.length > 0) { - for (const server of workflowMcpServerIds) { - mcpPubSub.publishWorkflowToolsChanged({ - serverId: server.id, - workspaceId, - }) - } - } - - return { - archived: !workspaceRecord.archivedAt, - workspaceName: workspaceRecord.name, + if (mcpPubSub) { + for (const serverId of serverIds) + mcpPubSub.publishWorkflowToolsChanged({ serverId, workspaceId }) } } diff --git a/apps/sim/lib/workspaces/organization-workspaces.integration.ts b/apps/sim/lib/workspaces/organization-workspaces.integration.ts index b0fd0397d0a..331b66d33b8 100644 --- a/apps/sim/lib/workspaces/organization-workspaces.integration.ts +++ b/apps/sim/lib/workspaces/organization-workspaces.integration.ts @@ -26,6 +26,7 @@ const database = drizzle(connection, { schema }) beforeAll(async () => { await connection.unsafe(`CREATE SCHEMA "${schemaName}"`) await connection.unsafe(` + CREATE TABLE project_workspace (project_id text NOT NULL, workspace_id text UNIQUE NOT NULL); CREATE TABLE member (id text PRIMARY KEY, organization_id text, user_id text, role text); CREATE TABLE organization (id text PRIMARY KEY, storage_used_bytes bigint NOT NULL); CREATE TABLE invitation ( diff --git a/apps/sim/lib/workspaces/organization-workspaces.ts b/apps/sim/lib/workspaces/organization-workspaces.ts index 8659ab20424..2db7b69675d 100644 --- a/apps/sim/lib/workspaces/organization-workspaces.ts +++ b/apps/sim/lib/workspaces/organization-workspaces.ts @@ -14,6 +14,7 @@ import { changeWorkspaceStoragePayersInTx } from '@/lib/billing/storage/payer-tr import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx, DbTransaction } from '@/lib/db/types' import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' +import { transferWorkspaceProjects } from '@/lib/projects/membership' import { invalidateWorkspaceTableLimitsCache } from '@/lib/table/billing' import { getOrganizationOwnerId, WORKSPACE_MODE } from '@/lib/workspaces/policy' @@ -351,6 +352,7 @@ export async function attachOwnedWorkspacesToOrganizationTx( } } + await transferWorkspaceProjects(tx, ownedWorkspaceIds, organizationId) const now = new Date() await changeWorkspaceStoragePayersInTx( tx, @@ -492,6 +494,7 @@ export async function detachOrganizationWorkspacesTx( const workspaceIds = organizationWorkspaces .map((organizationWorkspace) => organizationWorkspace.id) .sort() + await transferWorkspaceProjects(tx, workspaceIds, null, organizationOwnerId ?? undefined) await lockWorkspaceRowsForPayerChanges(tx, workspaceIds) const payerChanges = organizationWorkspaces.map((organizationWorkspace) => ({ workspaceId: organizationWorkspace.id, diff --git a/docs/plans/project-backfill-runbook.md b/docs/plans/project-backfill-runbook.md new file mode 100644 index 00000000000..d5154830401 --- /dev/null +++ b/docs/plans/project-backfill-runbook.md @@ -0,0 +1,68 @@ +# Project backfill operations + +The SQL migration creates empty Project tables during the normal migration job. This backfill is a separate post-deployment operation. It is not registered in the startup migration registry: old replicas are still serving when that registry runs. + +## Operator execution + +Run the CLI from an approved maintenance host or a one-off migration container matching the deployed application revision. There is no dedicated GitHub Actions workflow and no automatic migration-registry entry for this backfill. The normal deployment pipeline still applies the additive SQL migration. + +Before apply: + +- Finish deploying the compatible release, including draining old workers. Pause manual lineage/ownership maintenance and coordinate a window without concurrent deployments. Without a workflow wrapper, the operator owns these checks; the CLI does not verify ECS cutover or serialize deployment pipelines. +- Supply `PROJECT_BACKFILL_DATABASE_URL` through the approved secret mechanism. Use a direct session-capable primary connection, with no pooled application DSN fallback. Verify the environment and the fingerprint printed by `identity`. +- Use a persistent, writable report directory. Retain the exact code revision, dry-run plan, apply reports, verification reports and console output with the release record. Reports contain resource metadata and should have restricted access. +- Rehearse staging using the same command before production. This task has not executed a hosted backfill or established an existing maintenance host. + +1. Run `dry-run` and inspect the JSON report: roots, environment IDs, planned Project IDs, proposed names, changes and conflicts. +2. Repair reported conflicts deliberately and produce a fresh dry run. Apply refuses incomplete or conflicted plans. +3. Run `apply --from-file --database-id --writers-drained`. Keep the same reviewed code revision and original plan for retries. The acknowledgement does not stop writers; each transaction applies one planned family after checking for changes. +4. Run `verify` independently. Require `completed: true`, `ready: true`, zero conflicts and zero missing assignments. Apply success alone does not establish readiness. +5. Repeat verification after normal writes resume. Keep Project/Issues consumers disabled until reconciliation and the separately deployed database membership/minimum-environment constraints are complete. This command never flips an activation flag. + +A canceled or failed apply may have committed earlier families. Retain its report and original dry-run artifact; reapply the same artifact after checking the failure. Existing matching planned IDs are reused. A changed code revision requires reviewing compatibility and normally a new dry-run plan. Never produce a new plan merely to hide an unexpected assignment. + +## Safety and failure handling + +- Dry-run and verify do not change database rows. All modes take a session advisory lock so two operator runs cannot overlap. The reserved connection and backend PID are checked before each batch; losing that connection stops the run. +- Discovery pages contain 100 workspace IDs. Each family defaults to at most 1,000 members at the CLI, with an explicit maximum of 10,000. Total discovery is capped at 250,000 workspaces; lineage ancestry is capped at 1,000 links. Oversized data needs a reviewed adjustment or separate migration, never an unbounded run. +- Apply takes `SHARE ROW EXCLUSIVE ... NOWAIT` locks on workspace, Project and membership tables before rediscovering the family. This excludes concurrent inserts/deletes/updates for the short transaction, including new descendants. Reads remain available. Busy writers cause immediate contention rather than an indefinitely queued lock. This is a maintenance operation: ordinary workspace writes can briefly wait while a batch holds its locks. +- A SHA-256 fingerprint covers the ordered environment records, including names, owners, organization, archive timestamps and fork connections. Any change since dry-run rejects that family. Existing Project assignments must match the planned Project ID. Existing names are retained. +- Reads retry transient connection/unavailability failures with bounded jitter. Writes retry only SQLSTATE `40001` (serialization), `40P01` (deadlock) and `55P03` (lock unavailable), at most three attempts. No connection-error write retry: the commit result may be unknown. Constraint errors, cancellation, statement timeouts and unknown errors stop the run. +- Lock timeout is 2 seconds; statements and transactions are capped at 30 seconds. PostgreSQL 17+ is required for the transaction timeout. Runtime is bounded between batches (default 10 minutes, maximum one hour), so the last transaction may finish after the budget. SIGINT/SIGTERM stop after the current transaction; forced termination rolls back an open transaction. +- Reports are replaced atomically before any write and after every committed family. Counts reflect acknowledged commits only. If the connection disappears at commit, counts can underreport; reconcile through the original plan and verification. A failed report write stops further batches; the last durable report remains available. Do not infer rollback from a missing final report. +- Reports distinguish planned counts in dry-run/verify from committed counts in apply. `completed` means the selected scan/plan was processed; `ready` is set only by successful verification. SQLSTATE is recorded without dumping database error payloads or credentials. + +## Conflict repair + +Report entries include the root/environment IDs, relevant Project IDs and reason. Examples: + +- Multiple Projects in one connected family: inspect intended lineage and retained data; repair membership or disconnect the intended subtree transactionally. Never automatically merge Projects. +- Cross-organization lineage: resolve the workspace ownership/lineage first using the approved organization lifecycle operation. +- Changed family after dry-run: inspect the change, then produce and review a new plan. +- Empty Project or archive mismatch: determine the intended retained environments and lifecycle state. Do not silently delete Project data or unarchive environments. +- Cycle or missing parent: repair the fork connection before migration. + +The runner does not repair anomalies on the operator's behalf. After repair, run dry-run and verify again. Membership uniqueness and foreign keys remain active throughout. + +## Self-hosted execution + +Use the migration image from the same release as the application, after all application/worker replicas run compatible code. PostgreSQL 17+ and a direct session-capable primary connection are required. Supply `PROJECT_BACKFILL_DATABASE_URL` through your deployment's secret mechanism; do not place it in shell history or command arguments. Mount a writable, persistent report directory owned by the container's non-root user. + +Inside that image, the working directory is `/app/packages/db`: + +```sh +bun --no-env-file scripts/backfill-projects.ts identity +PROJECT_BACKFILL_REPORT_PATH=/reports/plan.json \ + bun --no-env-file scripts/backfill-projects.ts dry-run +PROJECT_BACKFILL_REPORT_PATH=/reports/apply.json \ + bun --no-env-file scripts/backfill-projects.ts apply \ + --from-file /reports/plan.json --database-id --writers-drained +PROJECT_BACKFILL_REPORT_PATH=/reports/verify.json \ + bun --no-env-file scripts/backfill-projects.ts verify +``` + +Override the migration container command for this one-off operation; do not change the ordinary `db:migrate` startup command. Apply the same report review, writer coordination, replay and activation rules above. Keep the original plan on persistent storage until verification and the release are complete. + +## Local proof + +`packages/db/scripts/project-backfill.integration.ts` creates a separate disposable database for every case, seeds a pre-Project fixture and applies the actual `0393` SQL. It exercises real SQL and the CLI, including interruption, conflicting writers, lost sessions, report failures and idempotent replay. The normal integration workflow discovers this file and uploads the integration JSON result. Never aim this test at an existing app database; the shared test-infrastructure validator restricts the target to a local test database. diff --git a/docs/plans/project-entity-foundation.md b/docs/plans/project-entity-foundation.md new file mode 100644 index 00000000000..4126b389c63 --- /dev/null +++ b/docs/plans/project-entity-foundation.md @@ -0,0 +1,311 @@ +# Project entity foundation: implementation and rollout proposal + +Backend foundation implemented locally, with backfill hardening validated on 2026-10-01, following the approved planning review. Final feature review and hosted rollout remain separate. Database validation uses disposable test containers only. See the implementation and rollout notes below for the concrete scope. + +Migration `0393_project_foundation` follows `0392_dashboard`. The schema expansion and application writers belong in the foundation release; backfill and contract enforcement follow separately. + + +## Decisions and blockers + +Settled: Project and environment are separate identities; existing workspace IDs remain environment IDs. Issues belong to Project across environments. Project and environment names are independent. Workflow, table, file, knowledge and dashboard data remain environment scoped. Chat has no Project ownership or membership relation. Selecting a Project is navigation state, not execution authority. Ship backend/feature PRs before new UI. + +The user settled Project visibility and Issue access: any environment access makes the Project visible; the toggle lists only accessible environments. Issues are project-wide and all-or-nothing, available by default to anyone with access to any environment. Through permission groups, an org admin can disable all Issues for a Project for partial-access teammates (those lacking access to every environment in that Project). This replaces the earlier explicit Project-membership proposal for Issue access. + +1. **Access — settled:** derive navigation and default Issue access from current environment access. Apply the permission-group restriction only to partial-access teammates, for the target Project; no per-Issue or per-environment filtering of the Issue collection. Restricting Issues does not hide the Project or its accessible environments. Project rename/archive is allowed for org admins and workspace admins who administer every environment; Issue visibility alone does not grant administration. The partial-access Issue check excludes archived environments. +2. **Disconnect — settled:** disconnecting a fork moves it and all descendants into a newly created Project, atomically with unlinking the fork edge. The original Project retains its ID. Existing Issues stay in the original Project; the new Project starts empty. A user may explicitly ask Sim Chat/Mothership to migrate Issues later through an authorized operation, rather than coupling Issue migration to disconnect. +3. **Personal scope — settled:** Projects can exist without an organization. Organization deletion preserves each Project and all of its Issues together, retaining the Project ID and assigning an explicit personal owner; no Project/Issue cascade or replacement Project creation. + +Settled organization-deletion behavior: transfer each Project to an explicitly selected personal owner while retaining its ID and Issues, rather than splitting its Issues between former environment owners. Project archive cascades to every environment and workflow. An active Project must retain at least one active environment; users cannot individually delete/archive the last one. Whole-Project archive is the explicit exception, leaving an archived Project with archived environments, not an empty active Project. Cross-organization environment moves must explicitly separate the moved environment from the old Project or move the entire Project. Existing Issues stay with their original Project on fork disconnect; a later user-requested migration is a separate operation. These policies are particularly important for subscription cancellation and administrative organization deletion. + +No Project member table or grant backfill is required for the settled Issue-access model. Evaluate existing effective environment access and applicable permission-group policy at authorization time. Permission changes immediately affect access; do not materialize permanent Project grants. The Issue partial-access comparison uses non-archived environments, as confirmed by the user. Zero-active-environment Projects are permitted only in archived state, with their environment rows retained. Require at least one accessible active environment for ordinary active-Project access; use explicit archived-resource authorization for archive inspection/restore, never the empty all-environments predicate; do not treat an empty set as full access. External collaborators use their existing effective access, without introducing a new org-membership requirement solely for Issues. + + +Lifecycle behavior: existing Issues remain with the original Project after disconnect; explicit migration can be requested through Sim Chat/Mothership. Org admins or callers with admin on every environment may rename/archive. Archived environments are excluded from the Issue partial-access comparison. The separate all-environment administrative rule includes archived environments; the exclusion applies specifically to Issue access. + +Organization deletion — settled: preserve each Project and its Issues together and assign an explicit personal owner, matching the existing workspace-preservation pattern. Do this atomically with workspace detachment and organization deletion; do not rely on a bare SET NULL FK or infer authority from the billing payer. Detachment assigns the current organization owner as the Project lifecycle owner, matching workspace detachment. If the organization has no owner, the Project retains its required existing lifecycle owner. The FK prevents unresolved user ownership. Subscription lapse is non-destructive as well; it must never reuse a Project/Issue purge path. + +## Organization-deletion preservation contract + +Verified at this planning baseline: + +- `apps/sim/app/api/v1/admin/organizations/[id]/route.ts:302`: transaction calls detach, organization-resource cleanup enqueue, then org delete. Its deletion contract explicitly says workspaces survive; subscriptions referencing the org block deletion. +- `apps/sim/lib/workspaces/organization-workspaces.ts:426`: detach selects organization-mode workspaces without excluding archived rows, clears organization via payer transfer, sets mode to `grandfathered_shared`, and grants admin to the new billing account (org owner if present, otherwise workspace owner). This is not a workspace purge. +- `apps/sim/lib/organizations/resource-cleanup.ts`: captures directly organization-owned files/chats before FK cascade in a transactional outbox. It is not a blanket workspace-resource cascade. +- `apps/sim/lib/auth/auth.ts:1773`: Better Auth organization deletion is disabled. +- `apps/sim/lib/billing/webhooks/subscription.ts:140`: subscription dormancy calls workspace detach, retaining the org. Reusing this helper must not accidentally delete Projects/Issues when a subscription lapses. + +Enumerate affected Projects under the organization lifecycle lock before changing workspace organization/membership. Compare `project.organizationId` with canonical membership/workspace ownership, report inconsistent or mixed-tenant Projects, and include archived org Projects and report legacy empty-Project anomalies that a workspace-only join would miss. Resolve and validate an explicit personal owner for each Project, update ownership to personal, and detach its environments in the same transaction before the organization is deleted. Preserve Project IDs, membership rows, Issues and Issue attachments; do not enqueue them for org-resource cleanup. Existing direct org chat/file cleanup remains separate. Retire organization-specific permission-group bindings explicitly after scope transfer; personal access then uses current effective environment access under the agreed policy. Do not copy grants or restrictions from unrelated orgs. A transfer must not leave a surviving Project referencing a deleted group or organization. Record old/new ownership and audit the actual operator, not the new owner or payer. Restrictive ownership FKs force the lifecycle operation to complete explicitly; they are not a reason to purge data. + +## Column-by-column schema proposal + +The preferred schema below uses environment-derived access and supports the accepted personal Project model. All new identifiers use `generateId()` and text columns, matching workspace and organization IDs. IDs are immutable and independent of names, lineage roots and deployment environment. + +| Column | Proposal and reason | +| --- | --- | +| `project.id` | Text PK, application-generated UUID v4. Never reuse a workspace ID; never recompute after backfill. | +| `project.name` | Non-null text; trim, require 1–100 characters in domain validation and enforce compatible DB bounds/nonblank check. Allow duplicate names; IDs disambiguate. No slug or case-insensitive unique-name constraint without a product requirement. | +| `project.organizationId` | Nullable FK to organization, `ON DELETE RESTRICT`. Null only for explicitly personally owned Projects. An explicit lifecycle transaction transfers Projects before organization deletion. Organization transfer must include archived Projects and surface legacy empty-Project anomalies too. | +| `project.ownerId` (new) | Required user FK, `ON DELETE RESTRICT`, retained on both personal and organization Projects, matching the workspace lifecycle-owner model. Lifecycle ownership, not implicit access. Anchors lifecycle ownership independently of environment owners; individual removal of the last environment is prohibited. Personal-owner deletion transfers or explicitly deletes the Project before user deletion. | +| `project.archivedAt` | Nullable timestamp; setting it archives the Project and all member environments/workflows through one compound operation. Preserve archived environment rows and Issues. Archived detail remains readable to authorized members; normal lists omit it unless requested; Project/Issue mutations reject except restore or approved cleanup. | +| `project.createdAt` | Non-null timestamp default now, immutable. Backfill records actual creation time; retain existing environment history rather than pretending the Project existed earlier. | +| `project.updatedAt` | Non-null timestamp default now; explicitly set by Project metadata/ownership/archive writes. `defaultNow` alone is not an update trigger. Environment membership changes are audited and update the affected environment timestamp; policy changes update their policy records. | +| root/default environment, icon/color, pipeline position, billing fields | Omit from foundation. None is needed for Issue identity or authority. A default execution environment, if added, must still be independently authorized. | + +Indexes: organization list index on `(organization_id, archived_at, id)`; personal-owner list index on `(owner_id, archived_at, id)`; sort by stable ID cursor initially, avoiding a name-based cursor that changes on rename. Add a name-sort index only if the selected list contract requires it. Foreign-key referencing columns need indexes for ownership/deletion lookup. Do not duplicate a simple organization index without demonstrated need. + +| Environment membership field | Proposal | +| --- | --- | +| `project_workspace.projectId` | Non-null text FK to Project. Prefer `ON DELETE RESTRICT` while environments remain, so deleting a Project cannot silently orphan environments. Changing membership on disconnect is an explicit transaction. | +| `project_workspace.workspaceId` | Non-null workspace FK with cascade on workspace deletion; unique index enforces at most one Project per environment. Use a composite PK `(projectId, workspaceId)` plus unique workspace index; Project-leading PK supports member lookup. | +| `project_workspace.createdAt` | Non-null timestamp default now, recording membership-row creation. On reassignment retain creation time and audit the move; introduce assignment timestamp only if required by an actual consumer. | +| `workspace.forkedFromWorkspaceId` | Keep existing lineage edge and `ON DELETE SET NULL`. Explicit disconnect creates a new Project; implicit edge removal on hard parent deletion does not silently move Issue ownership. | +| `workspace.organizationId` | Retain for compatibility/resource authorization. Proposed same-organization invariant with Project uses null-safe comparison and shared transactional scope changes; deferred constraint triggers validate final state across Project, membership and workspace writes. | + +**Membership storage:** a direct non-null `workspace.projectId` would enforce exactly-one membership more simply, but the accepted design uses `project_workspace`; do not implement both. The unique workspace index only enforces at most one. Add deferred constraint triggers on workspace insert and membership deletion/update to require one membership for every surviving workspace at commit after rollout. Lock/recheck the workspace when reassignment can race; deletion must allow cascaded removal for a workspace that no longer exists. Ship this enforcement only after compatible writers and backfill are complete. Require at least one environment membership for each Project and at least one active environment for each active Project. Archived Projects retain member rows but may have no active environments. Complement application checks with deferred final-state constraints after rollout; source and destination Projects must be checked on membership moves. + +Do not add `project_member` for navigation or Issues. Extend the existing permission-group model with a Project-targeted restriction on Issue access for partial-access teammates. Its absence means no extra restriction. Resolve the Project organization canonically; only its org administrators may configure it. Reuse existing group assignment and policy-composition rules instead of inventing a per-user grant table. The current resolver in `lib/permission-groups/config-scope.server.ts` is workspace-keyed; add intentional Project scope rather than passing a selected/root environment. Exact storage follows the permission-group skill during implementation. Project rename/archive authority is org admin OR admin on every environment; do not infer admin from an arbitrary environment. Never grant administration through an empty all-environments check. Active empty Projects are invalid; archived-resource operations authorize explicitly against retained environment grants and org administration. + +## Membership and lifecycle invariants + +1. Every environment, including archived ones, belongs to exactly one Project after enforcement. Each Project contains at least one environment; active Projects have at least one active environment. Whole-Project archive permits zero active environments while retaining archived member rows. +2. Environment and Project organization scopes agree. Personal environments within a Project may have different lifecycle owners only if one explicitly selected Project owner governs Project lifecycle; that must not transfer environment ownership or grant access. +3. Fork creation inherits its parent's Project in the same transaction after rechecking current source scope under lock. Existing fork families seed Projects initially. Explicit disconnection splits the subtree into a new Project. Multiple roots can still arise through parent deletion; do not infer Issue movement from root counts alone. +4. Nested forks inherit the same Project; siblings need not have a unique depth. A disconnected fork and every descendant move together to a fresh Project. Never manufacture a new Project from a missing-parent/backfill race. Reevaluate Issue eligibility on both Projects after the split: a previously partial-access user may become full-access. Project-targeted permission-group restrictions also need explicit migration semantics; proposed preserve applicable restrictions on the new Project rather than accidentally resetting configured policy, while acknowledging the all-or-nothing rule now applies to each new environment set. Record policy handling and both Project IDs in the operation audit. Existing Issues stay with the original Project. Explicit user-requested migration through Sim Chat/Mothership must authorize source and destination Projects under one semantic application operation, with atomic/retry-safe movement and audit; it is an Issues feature, not a Project-foundation dependency. +5. Renaming an environment does not rename the Project; rename the Project through its own operation. Backfill and new Project creation use ` - Project`, as specified by the naming contract; workspace names remain untouched. Bound the generated Project name without losing its suffix, with `Untitled` for an empty source; surface any length normalization in the report. Duplicates are allowed and recorded, not merged. +6. Individual environment delete/archive must leave at least one active environment in an active Project and at least one member environment overall. Block removal of the last environment; direct hard-delete, account deletion, moves and fork disconnect cannot bypass the invariant. Serialize on the Project so two concurrent removals cannot each see the other as the remaining environment. For disconnect/move, validate both resulting Projects; refuse a split that strands the original as active with no active environment. Coordinated whole-Project lifecycle operations are the explicit exception for active-count checks, not an adapter flag that callers can freely set. +7. Project archive is one authorized compound operation that archives every environment and workflow, including operational deactivation already owned by existing archive paths. Retain membership rows and Issues. Reject new environment/fork/workflow admission into an archived Project. Preserve pre-existing archived states. Current `lib/workspaces/lifecycle.ts` commits workspace/resource archival before calling `archiveWorkflowsForWorkspace`; looping this helper across environments would permit a partially archived Project. Refactor transaction-owning primitives so durable Project/environment/workflow state changes commit atomically, with existing required external effects queued/retried after commit. Failure must not be reported as complete while workflows remain active. Project restore is deferred until a complete inverse exists; do not ship a metadata-only unarchive that leaves no active environments or silently revives resources archived before the Project. Archival does not imply cancellation of already-running executions beyond existing archive semantics unless separately specified. +8. Moving one environment between organizations cannot retain its old Project membership. For initial shipment, refuse a partial Project transfer unless the operation explicitly names a destination/new Project and defines the Issue outcome; preserve original Project Issues. A whole-Project transfer requires authorization and disclosure for all environments, retained Project data, collaborators and Issues. +9. Organization cancellation/detach/deletion must process Projects and all member environments atomically with existing payer transfers. The legacy per-owner workspace detach cannot silently determine how to split Project-wide data. If a complete, authorized transfer cannot be determined, stop before committing rather than silently orphaning or broadening access. +10. Project hard delete is not required to unblock Issues. Restrict it while environments or Issues exist. `issue.projectId` should be NOT NULL with restrictive deletion until an explicit purge policy is implemented. Deleting an account must account for personally owned Projects and retained Issues, not merely surviving workspace grants. + +## Authorization and application surface + +Provide a shared canonical Project application context: `{ projectId, organizationId, ownerId, archivedAt }`, loaded by ID, with optional asserted organization/workspace scope compared to canonical data. Return not-found for hidden/mismatched scope, consistent with shared concealment policy. Loading must be inside the authorized application-use-case lifecycle; an HTTP/tool adapter may not query protected membership itself. + +Create a small shared Project authorization wrapper alongside the existing workspace/organization wrappers. It must reject unsupported Principal kinds before loading protected data; authorize current effective environment access and applicable Project-scoped permission-group policy; distinguish Issue use from Project administration; respect archive state and typed errors; project semantic audit from authoritative results; and keep external side effects after success. Mutations lock and recheck Project state/authority sufficiently to serialize with environment access revocation, environment-set changes, policy changes, organization transfer and archive. Persist a durable audit/outbox record inside the transaction where existing shared infrastructure supports it; do not report a committed mutation as absent because a later notification fails. + +Do not use `defineAuthorizedWorkspaceUseCase` with an arbitrary root environment. Staging has both `defineWorkspaceOperation` and `defineOrganizationOperation`; the latter currently accepts session/organization-delegated principals and cannot model personal Project scope alone. Extend the shared foundation intentionally, rather than hiding authorization in `projects.ts` or borrowing a payer identity. + +Proposed first contract (session surface; supported principal expansion is explicit): + +| Operation | Authority and behavior | +| --- | --- | +| `projects.list` | Paginated Projects containing an accessible environment, in requested owner scope; archived Projects require a separate explicitly authorized archive listing, never vacuous environment-access checks; archived filter. No inaccessible environment IDs, names, counts or fork parent references. | +| `projects.get` | Basic navigation metadata visible through an accessible environment (archived-resource access uses a separate explicit policy); Issue and administrative capabilities checked separately; stable `{ id, name, organizationId, archivedAt, createdAt, updatedAt }` plus caller capabilities. | +| `projects.resolveForWorkspace` | Authorize environment access; it permits resolving basic Project navigation metadata through the canonical `project_workspace` membership, without requiring a separate Project grant. Never accept a supplied Project ID as truth. | +| `projects.listEnvironments` | Project navigation visibility plus each environment's existing access policy; filter hidden parents in lineage output. Paginate rather than embedding all environments in every Project result. | +| `projects.create`, `rename`, `archive` | Creation atomically creates the first environment under existing creation/quota policy. Rename/archive require org admin or admin on every environment; archive cascades to all member environments/workflows. Do not automatically add member grants. No zero-environment creation endpoint. Restore is deferred until full environment/workflow lifecycle behavior is defined. | +| `projects.addEnvironment` | One compound use case authorizing Project administration and existing workspace-creation policy, then atomically creating environment with correct Project membership. Legacy workspace creation still creates its own Project. | +| Project Issue restriction configuration | Existing permission-group application operations extended with Project scope; org-admin authorization and canonical group/Project organization matching. No separate Project membership CRUD. | +| Issue operations | Use the all-or-nothing Issue gate below; action-specific mutation policy remains to be agreed without introducing partial Issue visibility; canonical Issue lookup verifies `issue.projectId`. No environment ID required for Issue CRUD. Links to environment resources are individually authorized on resolution. | + +Issue authorization for a human caller uses the complete canonical environment set E and the subset A for which the caller currently has effective access. The server must compute E independently of the filtered environment-toggle response: + +```text +canViewProject = |A| > 0 +hasPartialEnvironmentAccess = |A| > 0 AND |A| < |E| +canUseProjectIssues = canViewProject + AND NOT (hasPartialEnvironmentAccess AND effectiveGroupPolicyDisablesIssuesForThisProject) +``` + +The agreed truth table is: no accessible environment -> no default Issue access; some environments -> all Issues by default, none when the restriction applies; all environments -> the partial-access restriction does not deny Issues. Normal authentication, principal-kind and archive/action rules still apply. Return Issue capability separately from navigation capability. Never return hidden environment names/IDs/counts merely to explain a denial. + +Enforce this once in the shared Project Issue authorization path used by list/detail/mutations, search, counts, notifications/subscriptions, exports and tools. A denied caller must not receive Issue titles or other Issue data through side channels. Existing environment resources linked from Issues still require their own environment checks; access to all Issues never grants production access. Ordinary Issue mutation roles belong to the Issues implementation, but may not create a per-environment Issue subset. Project rename/archive uses org-admin OR all-environment-admin authority; permission-group edits remain org-admin operations. + +Creation/deletion/archive/restoration/move of environments and access grants/revocations can change partial/full status. Reevaluate on the server and invalidate relevant capability/policy caches and live subscriptions. Serialize mutating checks with changes that can alter the decision. Do not select the policy using whichever environment the user toggled to. Missing policy defaults to allow, but a policy lookup failure must propagate as an error, not be interpreted as an absent restriction. Archived environments are excluded from this Issue access comparison, as confirmed by the user. + +Start Project HTTP routes at `/api/projects` and `/api/projects/[id]`, with named contracts under `lib/api/contracts/projects.ts` and shared builders. Add environment lookup and permission-group configuration routes as needed by the first consumer. Query hooks use `requestJson`, named contract types and React Query. Start with a session-only Project API; do not advertise unsupported key/delegation support. + +Workspace API keys and workspace-scoped executor/system principals **must not** gain project-wide Issue access just because their environment points at the Project. Personal API keys/OAuth need an explicit Project credential policy (there is no unambiguous environment `allowPersonalApiKeys` switch at this scope), scope enforcement and permission-group checks before v2 Project routes ship. Organization delegation already exists, but is documented/search-scoped; extend trusted audience/operation policies and recheck current subject membership before admitting Project operations. Org chat Project selection is a requested target only. No chat-to-Project relation, implicit default environment, or copied grant. + +The Issues implementation can build on the foundation contract. Existing environments become usable for Issues only after validated backfill/enforcement; new properly provisioned Projects can be exercised earlier behind a release gate. No promise of a production-ready ID resolver that invents IDs on reads. + +## Required code-path inventory + +Paths below are relative to the repository root. `Migrate` means membership/authorization/lifecycle behavior is in scope; it does not mean copying prototype implementations wholesale. + +| Path / ingress | Classification and required work | +| --- | --- | +| `packages/db/schema.ts`, `migrations/`, `migrations/meta/_journal.json` | Migrate: additive Project/environment-membership schema and Project-scoped permission-group restriction, later constraints; preserve staging's dashboard migration. | +| `packages/db/scripts/migrate.ts`, `script-migrations/index.ts`, `script-migrations/types.ts` | Migrate integration: SQL runs before registered scripts under a session migration lock. This lock does not stop application writers. Do not put a large unbounded backfill in startup. | +| `apps/sim/lib/workspaces/create.ts` | Migrate both `createWorkspaceInTransaction` and wrapper; Project, membership and workspace created atomically. Its `createDefaultPersonalWorkspaceInTransaction` covers enterprise owner-claim provisioning. | +| `apps/sim/app/api/workspaces/route.ts` | Migrate normal POST and lazy/default workspace creation through a shared authorized application creation operation; preserve legacy wire behavior and existing workspace URLs. | +| `apps/sim/lib/workspaces/application/create-organization-workspace.ts`; `lib/mothership/application/execute-organization-workspace-use-case.ts`; `lib/mothership/tools/server/workspaces.ts` | Migrate shared lower creation path; keep Mothership a trusted adapter and preserve org pinning, capabilities, billing admission and resource refresh behavior. | +| `apps/sim/lib/billing/enterprise-owner-claim.ts` | Migrate inherited transaction behavior; no nested independently committing Project creation. | +| `apps/sim/ee/workspace-forking/lib/create-fork.ts`; `application/`; `lib/lineage/{lineage,lineage-root,unlink}.ts` | Migrate Project inheritance/validation under existing rank-ordered lineage and row locks. Disconnect atomically creates a new Project and reassigns the complete descendant subtree. Repeated/concurrent unlink must not create duplicate Projects; preserve existing edge-lock ordering and reread the closure. Do not add an organization lock held across the entire copy without reconciling lock order. | +| `apps/sim/app/api/workspaces/[id]/route.ts`; `lib/workspaces/lifecycle.ts` | Migrate required use-case boundaries; environment rename remains independent, DELETE is archive; guard last-active-environment removal. Add Project-wide atomic archive primitives while preserving existing resource archival/deactivation semantics and retryable effects. | +| `apps/sim/lib/workflows/lifecycle.ts` | Account-related bulk workspace archive must obey Project last-environment invariants; full Project archival must enlist workflow state in the same compound operation. Transfer surviving Projects on account deletion or use an explicit whole-Project lifecycle path. Workflow restore is not workspace restore. | +| `apps/sim/lib/workspaces/organization-workspaces.ts` | Migrate attach/detach closure, locks, Project scope and personal-owner transfer together with existing payer changes; include archived environments/Projects and report legacy empty-Project anomalies. | +| `apps/sim/lib/invitations/core.ts`; `lib/billing/organization.ts`; `app/api/v1/admin/organizations/[id]/members/route.ts`; `scripts/consolidate-users-into-organization.ts` | Required attach callers. Reuse corrected transaction primitive; recompute Project visibility/Issue eligibility after environment grants or scope changes. Do not broadly refactor v1 APIs. | +| `apps/sim/lib/billing/webhooks/subscription.ts`; `app/api/v1/admin/organizations/[id]/route.ts` | Required detach/cancellation/delete callers; explicit Project lifecycle before restrictive org FK. Preserve atomic detach+delete and billing semantics. | +| `apps/sim/lib/workspaces/admin-move.ts`; `admin-move-source-impact.ts`; `lib/admin/member-operation.ts`; `lib/billing/enterprise-provisioning.ts`; `app/api/v1/admin/dashboard/workspaces/[id]/move/route.ts` | Migrate transfer preflight/disclosure, Project conflicts and transaction ownership. Existing moves can dissolve fork edges; that alone cannot define Project transfer. Keep admin operation receipts and recoverable audit. | +| `apps/sim/lib/billing/organizations/membership.ts` | Migrate member removal, external removal, member transfer and organization-owner transfer to invalidate Project Issue eligibility and handle personal lifecycle ownership alongside workspace permission rows. | +| `apps/sim/lib/users/account-deletion.ts` | Migrate deletion planning/rechecks/transaction for retained Issue data, owned Projects and last-environment guards, transfers and Project FK restrictions. Direct workspace deletion uses alias `workspaceTable`; a search only for `delete(workspace)` misses it. | +| `apps/sim/lib/workspaces/{list,public-queries,host-context,seed-workspace-list}.ts`; `application/list-*.ts`; `packages/platform-authz/` | Preserve existing environment authority. New Project reads must not replace resource scope or turn Project membership into environment permission. Add owner-context resolution only where needed. | +| `apps/sim/lib/workspaces/application/{manage-permissions,remove-member}.ts`; `permissions/`; `access/workspace-access.ts`; invitations and organization member operations | Required access inventory. Reevaluate any/all environment access at invite/removal and update live Issue access; do not materialize permanent Project grants. | +| `apps/sim/app/api/v2/workspaces/**`; `lib/api/contracts/v2/`; `lib/api/mcp/generated/v2-operations.ts`; SDK/CLI generation | Preserve existing environment API. Project APIs are an additive surface after credential policy, using the same Project application operations. Regenerate descriptors only when adding that surface. | +| `apps/sim/app/api/v1/admin/workspaces/[id]/{import,export}/route.ts`; `lib/workflows/operations/import-export.ts`; `lib/workspaces/__integration__/mapped-import.integration.ts` | Reviewed scope: import works into an existing environment, not a new Project. Preserve identity and never import Project grants/IDs from resource payloads. No broad v1 migration. | +| `apps/sim/background/cleanup-soft-deletes.ts`; `lib/billing/storage/{payer-transfer,tracking}.ts`; `lib/mothership/inbox/{lifecycle,settings-store}.ts` | Reviewed adjacent writers: resource cleanup, payer/accounting and inbox settings are not Project identity changes. Project archive must invoke established environment/workflow deactivation; preserve existing retention policy and do not add immediate hard purge. No blanket edits to every workspace update. | +| `apps/sim/lib/permission-groups/{config-scope.server,resolve.server,fields,mutation}.ts`; `application/`; group contracts/settings UI | Extend Project-scoped partial-access Issue restriction through existing authorized group operations. Default absent setting to allow; preserve other fields on update; resolve all applicable groups using existing composition rules. Validate group/Project organization and invalidate Project Issue capabilities on edits. Read `add-permission-group-item` before implementation. | +| New `apps/sim/lib/projects/application/**`, contracts, internal routes, hooks | Implement fresh against staging shared foundations. Session operations first; add other adapters only with intentional Principal policy. | +| Prototype `lib/mothership/chat/lifecycle.ts`, `chat/application/fork.ts`, `chat/list-mothership-chats.ts` and `copilot_chat_project` | Non-goal: exclude chat relation imports, writes, filters, backfill and schema. Separate chat-ownership work removes obsolete prototype relations if ever deployed there. | +| Prototype `app/o/[organizationId]/p/hooks/use-projects.ts` and resource-browser/UI port | Defer: consume true Project IDs only after foundation. Extract final behavior later; no prototype cherry-picks. | + +Implementation must repeat the write-ingress search after rebasing (including schema aliases/raw SQL, user cascades, scripts and fixture writers). This is a source-based inventory, not a claim that production rows already satisfy the invariants. + +## Backfill and deployment procedure + +### Expand and capture a plan + +1. Ship additive Project and membership tables without enforcing membership completeness yet; keep current UI/API functional. Do not rewrite workspace names, routes, resource IDs or chat rows. Ship compatible writers in the same foundation release, but account for old replicas during rollout. Backfill activation and final constraints are separate deployment steps. +2. Use `packages/db/scripts/backfill-projects.ts`, with explicit `PROJECT_BACKFILL_DATABASE_URL`, `PROJECT_BACKFILL_REPORT_PATH`, and `dry-run` / `apply --from-file --database-id --writers-drained`. Run with `bun --no-env-file` so a checkout’s `.env` cannot select the database. The operator must verify the supplied target; there is no implicit application-DSN fallback. +3. Scan all workspaces, including archived, in stable ID keyset pages. Discover graph components with cycle detection and bounded traversal; roots are rows with no parent, missing parents are reported, cycles and organization mismatches are blocking anomalies. Keep only one bounded family in memory. Committed Project and membership rows serve as the durable resume mapping. Never silently reconnect already detached lineages or group unrelated roots by equal name. +4. Group existing environments by their current fork connections. A root workspace and all forks still connected beneath it become one Project. For example, Production → Staging → Dev becomes one Project containing all three environments. If Staging was previously disconnected from Production, create two Projects: one for Production, and one for Staging plus Dev. A workspace with no connected forks gets its own Project. Each new Project uses the root workspace’s name plus “ - Project” and its `ownerId`. + + Preserve any valid Project assignment that already exists. If only some environments in the tree have been assigned to one compatible Project, add the remaining environments to that same Project. If the tree is assigned to multiple Projects or spans multiple organizations, report the conflict and leave it unchanged for review; do not automatically merge it. + + If all environments in the tree are archived, create an archived Project and use the most recent environment archive timestamp. Otherwise, create an active Project. Never reactivate an environment during migration. An existing Project with no environments is reported for repair because every Project must contain at least one environment. + +5. Allocate each proposed Project ID in the completed dry-run artifact with `generateId()`. Apply consumes that artifact and validates its database identity and family fingerprints. Reruns use the same planned IDs, including after an interrupted apply; compatible existing assignments are reused. No extra manifest table is required. +6. Duplicate names are allowed; bound and trim seed names deterministically and record before/after. An ID collision fails the batch; never silently replace a planned ID during apply. Never overwrite an unrelated Project on collision. No Project-member/grant backfill; existing groups gain an absent/default-allow restriction without changing existing settings. No chat backfill, no Issue backfill in this PR. + +### Coordinate writers and commit batches + +7. Before apply, drain old app/worker/script writers or use a bounded write-maintenance window for the affected lifecycle operations. The migration runner lock serializes migration runners only. A one-shot script registered in the startup runner may finish before old replicas stop creating unassigned environments and is not a readiness signal. +8. New create/fork/attach/detach/move writers and the backfill must share a documented lock order. Integrate with existing organization, invitation, payer, lineage and workspace locks; do not introduce an opposite Project→organization order. Lock scope owners/Project/family according to that rank plan, then member rows in stable code-unit ID order, re-read closure and fingerprints under lock, and retry if scope changed. For fork versus bulk attach/detach, the closure must be re-read after serialization so a newly committed child cannot be omitted. If online closure coordination cannot be proved, use the maintenance window for those operations. +9. Each normal family commits Project + all environment assignments atomically. Insert membership only for unassigned workspaces; a unique-workspace conflict requires a locked reread and validation of the expected Project, not silent success. Do not ignore unique conflicts. Keep counters/report progress after commit; counters must describe committed work. +10. Bound batches by rows and transaction duration, not only 200 roots. Oversized families are explicitly reported and processed in a dedicated controlled transaction/window, or through a staged assignment design whose incomplete state is hidden from Project consumers. Never expose a partially migrated Project as complete. Resume from the original dry-run plan and committed memberships; recover committed batches after process death without new IDs or accidental policy restrictions. +11. Keep legacy workspace UI operational while Project/Issues reads are release-gated until readiness. During rolling deployment a missing membership is an explicit not-ready state, not permission to create a new Project on a GET. A compatible fork writer must ensure its parent family transactionally, or temporarily refuse fork creation for an unassigned family. + +### Validate and enforce + +The command must emit counts and offending IDs for at least these checks (illustrative SQL for the proposed schema): + +```sql +-- Must be zero before exactly-one enforcement. +SELECT w.id FROM workspace w +LEFT JOIN project_workspace pw ON pw.workspace_id = w.id +WHERE pw.workspace_id IS NULL; + +-- Must be zero; use null-safe comparison for personal scope. +SELECT w.id, pw.project_id FROM workspace w +JOIN project_workspace pw ON pw.workspace_id = w.id +JOIN project p ON p.id = pw.project_id +WHERE w.organization_id IS DISTINCT FROM p.organization_id; + +-- Existing fork edges must stay in the same Project. +SELECT child.id FROM workspace child +JOIN project_workspace child_pw ON child_pw.workspace_id = child.id +JOIN project_workspace parent_pw ON parent_pw.workspace_id = child.forked_from_workspace_id +WHERE child_pw.project_id IS DISTINCT FROM parent_pw.project_id; + +-- No active Project may lack active environments. +SELECT p.id FROM project p +WHERE p.archived_at IS NULL AND NOT EXISTS ( + SELECT 1 FROM project_workspace pw JOIN workspace w ON w.id = pw.workspace_id + WHERE pw.project_id = p.id AND w.archived_at IS NULL +); + +-- Even archived Projects retain at least one environment row. +SELECT p.id FROM project p WHERE NOT EXISTS ( + SELECT 1 FROM project_workspace pw WHERE pw.project_id = p.id +); + +-- Every Project retains a user lifecycle owner. +SELECT id FROM project +WHERE owner_id IS NULL; + +``` + +Additionally validate FK orphans, cycles/missing parents, duplicate/partial manifest assignment, permission-group Project/organization mismatches, counts of archived environments, empty Projects, source permission changes since planning, and no Project membership implied by chat data. Empty Projects and active Projects with no active environments are blocking anomalies; repair them explicitly, never silently delete retained Issues. Duplicate names and fully archived Projects with retained environments are valid. Check plan/report counts against actual committed rows and ensure every blocked family is resolved before completion. + +12. After compatible writers are fully deployed and validation passes, add/validate constraints, enforce exactly-one membership, and enable consumers. Use staged `NOT VALID` validation for existing-row constraints where appropriate; lock-time-bounded DDL and staging's documented concurrent-index mechanism for large workspace indexes. A cross-table deferred trigger must validate both workspace and Project ownership updates and serialize conflicting writes; test concurrent transactions, not just final-state SQL. Ordinary FKs still own referential existence. +13. Repeat validation after a normal write interval. Confirm a fresh install and an upgraded self-hosted database reach the same ready state. An operator-run production backfill needs a documented self-hosted upgrade path; readiness must remain false until it completes. Do not silently hang all deploys on a production-sized graph scan. + +### Recovery and rollback + +Before enforcement/consumer activation, application rollback may ignore additive schema, but old writers require pausing the backfill and another reconciliation before reactivation. After exactly-one membership enforcement, rolling back to old writers is unsafe: they cannot create environments. Roll back only to the compatible foundation release, or deliberately relax the constraint with writes gated and rerun reconciliation afterward. + +Keep IDs, membership rows, and operator reports durable. If a backfilled grouping is wrong before Issues begins, correct it transactionally from the recorded mapping with an explicit reviewed repair. Once Issues reference a Project, do not undo by dropping Projects or regenerating IDs; use a forward repair with an explicit Issue retention/move decision. Preserve backups and reports. No automatic destructive down migration. Retry failed batches; committed family assignments survive. A script runner's completed-name marker is not a substitute for a row-level readiness check. + +## PR and dependency sequence + +| PR | Scope and dependency | Deploy boundary | +| --- | --- | --- | +| A — Project foundation | Clean implementation branch from fresh staging; schema expansion, environment-derived Project/Issue access and Project-scoped partial-access restriction, stable IDs and session contracts, shared creation/fork writes, required lifecycle compatibility, dry-run/apply tooling and focused proofs. No chat/UI. | Additive deployment; existing workspace UI works; Project consumer gate initially off. Project/environment placement are atomic. Required org/user lifecycle handling must not be deferred past activation. | +| B — migration enforcement | Depends on A: bounded apply runbook, validated data/readiness, constraints and activation. Can be a small separate PR/release, or a second deployment phase of A. | Must follow compatible-writer rollout and measured validation. No hardcoded success flag standing in for data integrity. | +| C — Issues | Issues stacks on A after access/lifecycle contract is agreed; merges against A and activates after B. Issue FK, application operations, routes and any feature UI independently reviewable; enforce the partial-access restriction across every Issue surface before enabling it. | Issue identity is Project ID; no environment-owner authorization fallback. | +| D — chat ownership | Independent of A/B/C. Org/personal chat ownership and resource browsing, with obsolete prototype Project references excluded. | No chat-to-Project relation or Project migration dependency. | +| E — remaining backend features | Resource-browser APIs, environment management and other feature foundations as separate PRs. Dashboards already exist in staging; do not reintroduce prototype migration. | Depends on A/B only where actual Project operations are used. | +| F — new organization/Project UI | Built on landed feature surfaces; extract completed behavior from the aggregate experiment. True Project ID and selected environment ID remain distinct. | Last; current workspace navigation remains supported until replacement is deliberately shipped. | + +If A becomes too large, split a prerequisite shared authorization/closure-lock fix from the entity PR, then stack A on it. Do not split out required lifecycle writes such that an enabled Project entity can silently corrupt on cancellation, invitation acceptance or account deletion. A schema-only merge can be additive, but is not the complete foundation required by Issues. + + +## Verification plan and release evidence + +Apply the `test-audit` authoring gate before code: enumerate failure modes, choose one strongest owner boundary, and avoid schema/registry/config assertions. Use disposable Postgres/Redis and an isolated app. Report to caller-supplied `PROJECT_FOUNDATION_REPORT_PATH` / `PROJECT_BACKFILL_REPORT_PATH` with each check's status, duration, failure and IDs; upload on CI failure. + +| Real boundary | Failure to prove | +| --- | --- | +| Migration against pre-Project DB snapshot | Active/archived singleton, siblings, nested forks, detached family, duplicate names, mixed owners, missing parent, cycle, cross-org family, prior partial/split assignments. Valid rows map once; anomalies block without mutation; rerun uses identical IDs and preserves default-allow policy. | +| Real concurrent DB transactions | Fork versus backfill; fork versus attach/detach/move; whole-Project archive versus fork/environment/workflow creation; two concurrent last-environment removals; archive versus Issue write; environment access revoke versus Issue write; full-to-partial transition versus Issue write; restriction edits versus cached reads; crash between batches. Assert committed scope and recoverability, not mocked call order. | +| Real create/provision/fork HTTP or application boundary | Workspace+Project rollback together after injected transactional failure; regular creation, first visit, enterprise claim and org Mothership creation all obey membership. | +| Real Project/Issue HTTP | Staging-only grant shows the Project and only staging in its toggle, with production hidden; partial-access user sees all Issues by default and none with the applicable group restriction; full-access user retains Issues under that restriction; no-environment user gains nothing; restriction changes do not hide accessible environments; list/detail/search/counts/notifications/tools honor denial; cross-org assertion concealed; workspace API key rejected; archived Project read allowed/write denied; active empty Project creation/removal is rejected; archived-state checks cannot grant access through an empty set; revocation effective without stale authorization cache. | +| Lifecycle E2E | Independent renames; disconnect creates one new Project for the complete subtree, retains original Project ID, and leaves existing Issues in the original Project; individual archive/delete of last environment is rejected; whole-Project archive covers every environment/workflow atomically and retains Issues; org cancellation/delete produces approved personal ownership or atomic refusal; user deletion handles retained Issues and last-environment invariants; transfer rollback preserves billing and scope. | +| Current UI compatibility | Existing workspace URLs, resource execution and import/export still work; no chat migration; Project selection cannot choose an execution environment implicitly. | +| Upgrade/install | Fresh install, interrupted backfill resume, rolling old/new writers before enforcement, enforced-release rollback boundary and self-hosted completion path. | + +Extend existing real suites where they own the failure: `lib/workspaces/__integration__/fork-sync.integration.ts`, `http-cli.integration.ts`, `mapped-import.integration.ts`, and `ee/workspace-forking/lib/lineage/fork-lock-order.integration.ts`; read their full coverage before deciding on additions. Project authorization and migration have new independent contracts and need real owner-boundary proofs. Existing type checks cannot prove concurrent ownership integrity. Demonstrate regressions go red with each relevant guard removed; never add a test that only restates a table definition. + +Implementation checks: relevant integration/E2E runs; workspace type-checks (app/db/auth/platform-authz as touched); `bun run check:api-validation:strict`; `bun run check:audits`; repository lint with review of autofixes; `git diff --check`. Local validation now includes 35 foundation/fork/lifecycle integration tests, 9 dedicated backfill/CLI integration cases, app/database type checks, lint, all 57 audits and diff checks. A negative control confirmed the changed-family guard catches a newly added descendant. These checks do not represent full coverage of every future-consumer scenario in the matrix above. No hosted backfill, browser rehearsal or production activation has occurred. + + +### Ownership clarification during implementation + +Every Project retains a required `ownerId`, including organization Projects. `organizationId` is optional; the two fields are not mutually exclusive. This matches workspace lifecycle ownership. Neither field substitutes for explicit workspace grants or organization-admin authority. A departing owner is transferred before account deletion; Project ownership does not cascade away with a user. Organization detachment keeps Project identity and explicitly assigns its personal lifecycle owner. + + +## Concrete implementation and rollout notes + +The schema contains `project` (`id`, `name`, required `ownerId`, nullable `organizationId`, `archivedAt`, `createdAt`, `updatedAt`) and `project_workspace` (`projectId`, unique `workspaceId`, `createdAt`). There is no stored fork depth, position, or Project billing state. + +Implemented session routes: + +- `GET /api/projects`: authorized, cursor-paginated active Project inventory. +- `GET /api/projects/[id]`: metadata, accessible active environments, and administration/Issues capabilities. +- `GET /api/projects/by-workspace/[workspaceId]`: canonical lookup that also authorizes the requested environment. +- `PATCH /api/projects/[id]`: rename. +- `DELETE /api/projects/[id]`: compound Project/environment/workflow archive. + +Workspace creation creates its Project atomically; fork creation joins the existing Project. These are the creation surfaces in this foundation. A separate empty-Project create, arbitrary environment attachment, restore UI, public-key API, and Issues CRUD are not implemented here. The existing permission-group editor gains the Project Issues restriction; the new Project navigation UI remains a separate consumer. + +`deniedPartialAccessProjectIssues` is a Project-ID denylist on existing permission-group config. An empty list allows access. For a partial-access teammate, each accessible active environment resolves its effective group using existing explicit-member/all-members/default precedence. If any of those effective groups denies this Project, all Issues are denied. The selected environment cannot change the answer. Archived environments are excluded from this comparison. Full-access teammates bypass this partial-access restriction. Issue use cases call `authorizeProject` in their own transaction before reading or writing Issue rows; `getProjectIssueAccess` is only a read-only probe. The Issue gate holds the permission-group lock through that transaction, so callers must respect its existing leaf-lock rule. + +Disconnect copies a matching restriction to the new Project. Moving a Project out of an organization removes its stale ID from that organization’s group configs. Rename/archive authorization includes every environment, including archived members. A departing organization member’s Project lifecycle ownership moves to the organization owner. Account teardown transfers surviving Projects to an administrator of their surviving environments and explicitly removes wholly private Projects in the same teardown transaction; it cannot leave a surviving active Project with no active environment. + +The backfill scans 100 workspaces per page, limits ancestor traversal to 1,000 links and CLI family batches to 1,000 environments by default (10,000 maximum), and commits one family at a time. It reports cycles, missing parents, oversized families, cross-organization lineages, conflicting assignments and inconsistent archive states. A reserved session advisory lock excludes overlapping backfill runs. Apply takes bounded NOWAIT table locks before rediscovering and validating the full family. Organization moves use a nonblocking Project lock because their legacy paths can already hold workspace locks; contention returns a retryable conflict. A conflicting batch leaves prior committed families intact and is safe to rerun after repair. + +Operator sequence: + +1. Deploy the additive migration and compatible writers. Do not enable downstream Project/Issues consumers yet. +2. Run dry-run with an explicitly verified database target and inspect its report. +3. Drain old writers and pause lineage/ownership maintenance while applying. Pass `apply --from-file --database-id --writers-drained`; this flag is an operator assertion, not an automatic drain. Legacy unassigned workspaces remain usable, but their fork operation returns a backfill-required conflict until assignment completes. +4. Rerun and verify zero new assignments and zero conflicts, then execute the reconciliation queries in this plan. Resolve any remaining unassigned/empty/mismatched rows before activation. +5. Enable consumers only after reconciliation. Complete database-level membership/minimum-environment enforcement in a later deployment, as recorded by `contract-pending` in the schema. Do not activate consumers on old-writer rollback without another reconciliation. + +Example invocation from the repository root (supply the target URL through your normal secret mechanism): + +```sh +PROJECT_BACKFILL_REPORT_PATH=/absolute/path/project-backfill-dry-run.json \ +bun --no-env-file packages/db/scripts/backfill-projects.ts dry-run + +PROJECT_BACKFILL_REPORT_PATH=/absolute/path/project-backfill-apply.json \ +bun --no-env-file packages/db/scripts/backfill-projects.ts apply \ + --from-file /absolute/path/project-backfill-dry-run.json \ + --database-id --writers-drained +``` + +Validation artifacts come from `PROJECT_FOUNDATION_REPORT_PATH` (or `apps/sim/test-results/project-foundation.json`) and the integration runner’s JSON report. CI already uploads `apps/sim/test-results/*.json`. The former mock-count workspace lifecycle tests were replaced by real database checks of concurrent removal and retry repair. The dedicated backfill suite executes the exact additive migration in a disposable database before exercising backfill. External webhook/socket/MCP cleanup retains existing best-effort post-commit behavior; durable archive state commits atomically. + +The operator-run CLI procedure, self-hosted commands, SQLSTATE handling, lock behavior, report interpretation and repair procedure are documented in [the Project backfill runbook](./project-backfill-runbook.md). diff --git a/knip.jsonc b/knip.jsonc index 4ab2873da7a..ea731f3397f 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -42,6 +42,8 @@ // Generated contracts mirror their source of truth; regenerating them must not // trip the unused-export ratchet, and hand edits would be overwritten. "lib/mothership/generated/**": ["exports", "types", "duplicates"], + // API conventions require exported named wire schemas and aliases, including before client adoption. + "lib/api/contracts/projects.ts": ["exports", "types"], "sandbox-tasks/index.ts": ["files"], "components/mcp/index.ts": ["files"], "triggers/quickbooks/index.ts": ["files"], diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 7cd5acb84d4..6505bf2431a 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -218,6 +218,8 @@ export const AuditAction = { WORKFLOW_EXPORTED: 'workflow.exported', WORKSPACE_CREATED: 'workspace.created', + PROJECT_UPDATED: 'project.updated', + PROJECT_ARCHIVED: 'project.archived', WORKSPACE_UPDATED: 'workspace.updated', WORKSPACE_DELETED: 'workspace.deleted', WORKSPACE_DUPLICATED: 'workspace.duplicated', @@ -287,6 +289,7 @@ export const AuditResourceType = { USER: 'user', WEBHOOK: 'webhook', WORKFLOW: 'workflow', + PROJECT: 'project', WORKSPACE: 'workspace', } as const diff --git a/packages/db/migrations/0393_project_foundation.sql b/packages/db/migrations/0393_project_foundation.sql new file mode 100644 index 00000000000..5a50b354418 --- /dev/null +++ b/packages/db/migrations/0393_project_foundation.sql @@ -0,0 +1,25 @@ +CREATE TABLE "project" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "organization_id" text, + "owner_id" text NOT NULL, + "archived_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "project_name_length" CHECK (char_length(btrim("project"."name")) BETWEEN 1 AND 100) +); +--> statement-breakpoint +CREATE TABLE "project_workspace" ( + "project_id" text NOT NULL, + "workspace_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "project_workspace_project_id_workspace_id_pk" PRIMARY KEY("project_id","workspace_id") +); +--> statement-breakpoint +ALTER TABLE "project" ADD CONSTRAINT "project_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project" ADD CONSTRAINT "project_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_workspace" ADD CONSTRAINT "project_workspace_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_workspace" ADD CONSTRAINT "project_workspace_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "project_organization_archive_id_idx" ON "project" USING btree ("organization_id","archived_at","id");--> statement-breakpoint +CREATE INDEX "project_owner_archive_id_idx" ON "project" USING btree ("owner_id","archived_at","id");--> statement-breakpoint +CREATE UNIQUE INDEX "project_workspace_workspace_id_unique" ON "project_workspace" USING btree ("workspace_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0393_snapshot.json b/packages/db/migrations/meta/0393_snapshot.json new file mode 100644 index 00000000000..9f358a08b88 --- /dev/null +++ b/packages/db/migrations/meta/0393_snapshot.json @@ -0,0 +1,29715 @@ +{ + "id": "ba7c9a7d-a315-42bc-9de6-931239aa626b", + "prevId": "b50de40a-fb8e-4b11-bd00-a0080b6ee7ef", + "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.agent_memory_turn": { + "name": "agent_memory_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_order": { + "name": "execution_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memory_turn_invocation_unique": { + "name": "agent_memory_turn_invocation_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memory_turn_workflow_idx": { + "name": "agent_memory_turn_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memory_turn_memory_id_memory_id_fk": { + "name": "agent_memory_turn_memory_id_memory_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memory_turn_workflow_id_workflow_id_fk": { + "name": "agent_memory_turn_workflow_id_workflow_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "workflow", + "columnsFrom": ["workflow_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 + }, + "browser_download_started_at": { + "name": "browser_download_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_started_at": { + "name": "execution_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_settled_at": { + "name": "execution_settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_owner_token": { + "name": "execution_owner_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_lease_expires_at": { + "name": "execution_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_revoked_at": { + "name": "execution_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "client_workflow_execution_id": { + "name": "client_workflow_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_processes": { + "name": "sandbox_processes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "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_organization_request_stops": { + "name": "copilot_organization_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_organization_request_stops_user_id_user_id_fk": { + "name": "copilot_organization_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_organization_request_stops_organization_id_organization_id_fk": { + "name": "copilot_organization_request_stops_organization_id_organization_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_organization_request_stops_user_id_organization_id_stream_id_pk": { + "name": "copilot_organization_request_stops_user_id_organization_id_stream_id_pk", + "columns": ["user_id", "organization_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_request_stops": { + "name": "copilot_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_request_stops_user_id_user_id_fk": { + "name": "copilot_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_request_stops_workspace_id_workspace_id_fk": { + "name": "copilot_request_stops_workspace_id_workspace_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_request_stops_user_id_workspace_id_stream_id_pk": { + "name": "copilot_request_stops_user_id_workspace_id_stream_id_pk", + "columns": ["user_id", "workspace_id", "stream_id"] + } + }, + "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 + }, + "tool_execution_version": { + "name": "tool_execution_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_admission_closed_at": { + "name": "tool_admission_closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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_chat_started_at_idx": { + "name": "copilot_runs_chat_started_at_idx", + "columns": [ + { + "expression": "chat_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_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" + }, + "copilot_runs_organization_id_organization_id_fk": { + "name": "copilot_runs_organization_id_organization_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_service_usage": { + "name": "copilot_service_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": false + }, + "worker_origin": { + "name": "worker_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_service_usage_pending_idx": { + "name": "copilot_service_usage_pending_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "delivered_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_task_subscriptions": { + "name": "copilot_task_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_task_subscriptions_execution_idx": { + "name": "copilot_task_subscriptions_execution_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_task_subscriptions_task_idx": { + "name": "copilot_task_subscriptions_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_task_subscriptions_chat_id_copilot_chats_id_fk": { + "name": "copilot_task_subscriptions_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_task_subscriptions_workspace_id_workspace_id_fk": { + "name": "copilot_task_subscriptions_workspace_id_workspace_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_task_subscriptions_user_id_user_id_fk": { + "name": "copilot_task_subscriptions_user_id_user_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "user", + "columnsFrom": ["user_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.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": { + "dashboard_workspace_id_unique": { + "name": "dashboard_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_created_by_user_id_fk": { + "name": "dashboard_created_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dashboard_updated_by_user_id_fk": { + "name": "dashboard_updated_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "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_kb_source_modified_idx": { + "name": "doc_kb_source_modified_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_modified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "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_processing_recovery_idx": { + "name": "doc_connector_processing_recovery_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "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": true, + "method": "btree", + "with": {} + }, + "doc_connector_processing_status_idx": { + "name": "doc_connector_processing_status_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "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\".\"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_connector_reconciliation_v2_idx": { + "name": "doc_connector_reconciliation_v2_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": 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": true, + "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_connector_tombstone_idx": { + "name": "doc_connector_tombstone_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NULL AND (\"document\".\"deleted_at\" IS NOT NULL OR \"document\".\"content_hash\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_live_idx": { + "name": "doc_connector_live_idx", + "columns": [ + { + "expression": "connector_id", + "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_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": true, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "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_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": {} + }, + "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": true, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "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_keyword_search": { + "name": "embedding_keyword_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 + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "embedding_keyword_search_kb_idx": { + "name": "embedding_keyword_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_keyword_search_document_idx": { + "name": "embedding_keyword_search_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_keyword_search_content_idx": { + "name": "embedding_keyword_search_content_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_keyword_search_id_embedding_id_fk": { + "name": "embedding_keyword_search_id_embedding_id_fk", + "tableFrom": "embedding_keyword_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding_keyword_tin": { + "name": "embedding_keyword_tin", + "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 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_keyword_tin_document_idx": { + "name": "embedding_keyword_tin_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "embedding_keyword_tin_id_embedding_id_fk": { + "name": "embedding_keyword_tin_id_embedding_id_fk", + "tableFrom": "embedding_keyword_tin", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "vector": { + "name": "vector", + "type": "halfvec(1536)", + "primaryKey": false, + "notNull": false + }, + "vector_384": { + "name": "vector_384", + "type": "halfvec(384)", + "primaryKey": false, + "notNull": false + }, + "vector_512": { + "name": "vector_512", + "type": "halfvec(512)", + "primaryKey": false, + "notNull": false + }, + "vector_768": { + "name": "vector_768", + "type": "halfvec(768)", + "primaryKey": false, + "notNull": false + }, + "vector_1024": { + "name": "vector_1024", + "type": "halfvec(1024)", + "primaryKey": false, + "notNull": false + }, + "vector_3072": { + "name": "vector_3072", + "type": "halfvec(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_document_lookup_idx": { + "name": "embedding_search_document_lookup_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"embedding_search\".\"enabled\"", + "concurrently": true, + "method": "btree", + "with": {} + }, + "embedding_search_cosine_hnsw_idx": { + "name": "embedding_search_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_512_cosine_hnsw_idx": { + "name": "embedding_search_512_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_512", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_cosine_hnsw_idx": { + "name": "embedding_search_384_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_cosine_hnsw_idx": { + "name": "embedding_search_768_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_cosine_hnsw_idx": { + "name": "embedding_search_1024_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_cosine_hnsw_idx": { + "name": "embedding_search_3072_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_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 (3)", + "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 + }, + "member_tombstone_cursor": { + "name": "member_tombstone_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "member_resurrection_cursor": { + "name": "member_resurrection_cursor", + "type": "text", + "primaryKey": false, + "notNull": 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 (3)", + "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 + }, + "detached_at": { + "name": "detached_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detach_reserved_bytes": { + "name": "detach_reserved_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "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 + }, + "scope_renewed_at": { + "name": "scope_renewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_cursor": { + "name": "scope_renewal_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_started_at": { + "name": "scope_renewal_started_at", + "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_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processing_dispatch_failed": { + "name": "processing_dispatch_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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_renewed": { + "name": "observations_renewed", + "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 + }, + "database_failure_class": { + "name": "database_failure_class", + "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')" + }, + "kcmsl_database_failure_class_check": { + "name": "kcmsl_database_failure_class_check", + "value": "\"knowledge_connector_member_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_partition": { + "name": "knowledge_connector_partition", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "partition_key": { + "name": "partition_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_served_at": { + "name": "last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure": { + "name": "failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "permission_cursor": { + "name": "permission_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_attempts": { + "name": "permission_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permission_retry_at": { + "name": "permission_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permission_last_served_at": { + "name": "permission_last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_started_at": { + "name": "permission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_failure": { + "name": "permission_failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcp_content_due_idx": { + "name": "kcp_content_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcp_permission_due_idx": { + "name": "kcp_permission_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_partition_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_partition_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_partition", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcp_pk": { + "name": "kcp_pk", + "columns": ["connector_id", "partition_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcp_partition_key_check": { + "name": "kcp_partition_key_check", + "value": "octet_length(\"knowledge_connector_partition\".\"partition_key\") BETWEEN 1 AND 1024" + }, + "kcp_context_check": { + "name": "kcp_context_check", + "value": "jsonb_typeof(\"knowledge_connector_partition\".\"context\") = 'object' AND octet_length(\"knowledge_connector_partition\".\"context\"::text) <= 16384" + }, + "kcp_status_check": { + "name": "kcp_status_check", + "value": "\"knowledge_connector_partition\".\"status\" IN ('pending', 'complete', 'blocked')" + }, + "kcp_cursor_check": { + "name": "kcp_cursor_check", + "value": "(\"knowledge_connector_partition\".\"cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"cursor\") <= 393216) AND (\"knowledge_connector_partition\".\"permission_cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"permission_cursor\") <= 393216)" + }, + "kcp_attempts_check": { + "name": "kcp_attempts_check", + "value": "\"knowledge_connector_partition\".\"attempts\" >= 0 AND \"knowledge_connector_partition\".\"permission_attempts\" >= 0" + } + }, + "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 + }, + "database_failure_class": { + "name": "database_failure_class", + "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": { + "kcsl_database_failure_class_check": { + "name": "kcsl_database_failure_class_check", + "value": "\"knowledge_connector_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "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.knowledge_projection_dirty": { + "name": "knowledge_projection_dirty", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content": { + "name": "content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marked_at": { + "name": "marked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_projection_dirty_marked_at_idx": { + "name": "knowledge_projection_dirty_marked_at_idx", + "columns": [ + { + "expression": "marked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_projection_dirty_document_id_document_id_fk": { + "name": "knowledge_projection_dirty_document_id_document_id_fk", + "tableFrom": "knowledge_projection_dirty", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": 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 + }, + "storage_version": { + "name": "storage_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "encrypted_context_summary": { + "name": "encrypted_context_summary", + "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()" + }, + "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_artifact": { + "name": "memory_artifact", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "memory_artifact_key_idx": { + "name": "memory_artifact_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_artifact_memory_id_memory_id_fk": { + "name": "memory_artifact_memory_id_memory_id_fk", + "tableFrom": "memory_artifact", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_artifact_key_execution_large_values_key_fk": { + "name": "memory_artifact_key_execution_large_values_key_fk", + "tableFrom": "memory_artifact", + "tableTo": "execution_large_values", + "columnsFrom": ["key"], + "columnsTo": ["key"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memory_artifact_memory_id_key_pk": { + "name": "memory_artifact_memory_id_key_pk", + "columns": ["memory_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_item": { + "name": "memory_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "memory_item_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "append_key": { + "name": "append_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_status": { + "name": "provenance_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_entries": { + "name": "provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_item_append_unique": { + "name": "memory_item_append_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "append_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_item_sequence_idx": { + "name": "memory_item_sequence_idx", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_item_memory_id_memory_id_fk": { + "name": "memory_item_memory_id_memory_id_fk", + "tableFrom": "memory_item", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_item_kind_check": { + "name": "memory_item_kind_check", + "value": "\"memory_item\".\"kind\" IN ('message', 'exchange')" + }, + "memory_item_provenance_status_check": { + "name": "memory_item_provenance_status_check", + "value": "\"memory_item\".\"provenance_status\" IN ('exact', 'unknown')" + } + }, + "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_resource_effects": { + "name": "mothership_resource_effects", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_id": { + "name": "effect_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_resource_effects_chat_id_copilot_chats_id_fk": { + "name": "mothership_resource_effects_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_resource_effects", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_resource_effects_chat_id_effect_id_pk": { + "name": "mothership_resource_effects_chat_id_effect_id_pk", + "columns": ["chat_id", "effect_id"] + } + }, + "uniqueConstraints": {}, + "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 + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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_access_request_settings": { + "name": "organization_access_request_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allow_requests": { + "name": "allow_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_access_request_settings_organization_id_organization_id_fk": { + "name": "organization_access_request_settings_organization_id_organization_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_access_request_settings_updated_by_user_id_fk": { + "name": "organization_access_request_settings_updated_by_user_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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_history": { + "name": "organization_search_history", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "organization_search_history_user_idx": { + "name": "organization_search_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_history_organization_id_organization_id_fk": { + "name": "organization_search_history_organization_id_organization_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_history_user_id_user_id_fk": { + "name": "organization_search_history_user_id_user_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_history_organization_id_user_id_pk": { + "name": "organization_search_history_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "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.organization_search_mcp_invocation": { + "name": "organization_search_mcp_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 + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "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_mcp_invocation_org_created_idx": { + "name": "organization_search_mcp_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_mcp_invocation_user_idx": { + "name": "organization_search_mcp_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_search_mcp_invocation_org_fk": { + "name": "org_search_mcp_invocation_org_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_search_mcp_invocation_user_fk": { + "name": "org_search_mcp_invocation_user_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_mcp_invocation_tool_check": { + "name": "organization_search_mcp_invocation_tool_check", + "value": "\"organization_search_mcp_invocation\".\"tool_name\" IN ('search', 'read_document', 'chat')" + }, + "organization_search_mcp_invocation_outcome_check": { + "name": "organization_search_mcp_invocation_outcome_check", + "value": "\"organization_search_mcp_invocation\".\"outcome\" IN ('success', 'error', 'cancelled', 'rate_limited')" + }, + "organization_search_mcp_invocation_duration_check": { + "name": "organization_search_mcp_invocation_duration_check", + "value": "\"organization_search_mcp_invocation\".\"duration_ms\" >= 0" + }, + "organization_search_mcp_invocation_client_name_check": { + "name": "organization_search_mcp_invocation_client_name_check", + "value": "length(\"organization_search_mcp_invocation\".\"client_name\") <= 256" + }, + "organization_search_mcp_invocation_auth_check": { + "name": "organization_search_mcp_invocation_auth_check", + "value": "(\"organization_search_mcp_invocation\".\"auth_kind\" = 'oauth_access_token' AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NOT NULL)\n OR (\"organization_search_mcp_invocation\".\"auth_kind\" IN ('personal_api_key', 'workspace_api_key') AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NULL AND \"organization_search_mcp_invocation\".\"client_name\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_secret": { + "name": "organization_secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_shared_unique": { + "name": "organization_secret_shared_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_member_unique": { + "name": "organization_secret_member_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_owner_idx": { + "name": "organization_secret_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_id_organization_secret_source_id_fk": { + "name": "organization_secret_source_id_organization_secret_source_id_fk", + "tableFrom": "organization_secret", + "tableTo": "organization_secret_source", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_secret_owner_user_id_user_id_fk": { + "name": "organization_secret_owner_user_id_user_id_fk", + "tableFrom": "organization_secret", + "tableTo": "user", + "columnsFrom": ["owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_secret_source": { + "name": "organization_secret_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "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": { + "organization_secret_source_org_unique": { + "name": "organization_secret_source_org_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_organization_id_organization_id_fk": { + "name": "organization_secret_source_organization_id_organization_id_fk", + "tableFrom": "organization_secret_source", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_secret_source_mode_check": { + "name": "organization_secret_source_mode_check", + "value": "\"organization_secret_source\".\"mode\" IN ('organization', 'member')" + } + }, + "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_access_request": { + "name": "permission_access_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "permission_access_request_pending_unique": { + "name": "permission_access_request_pending_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"permission_access_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_org_queue_idx": { + "name": "permission_access_request_org_queue_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "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": {} + }, + "permission_access_request_requester_idx": { + "name": "permission_access_request_requester_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "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": { + "permission_access_request_organization_id_organization_id_fk": { + "name": "permission_access_request_organization_id_organization_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_requester_id_user_id_fk": { + "name": "permission_access_request_requester_id_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["requester_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_decided_by_user_id_fk": { + "name": "permission_access_request_decided_by_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "permission_access_request_status_check": { + "name": "permission_access_request_status_check", + "value": "\"permission_access_request\".\"status\" in ('pending', 'fulfilled', 'declined', 'cancelled', 'closed')" + } + }, + "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.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_organization_archive_id_idx": { + "name": "project_organization_archive_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_archive_id_idx": { + "name": "project_owner_archive_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_owner_id_user_id_fk": { + "name": "project_owner_id_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_name_length": { + "name": "project_name_length", + "value": "char_length(btrim(\"project\".\"name\")) BETWEEN 1 AND 100" + } + }, + "isRLSEnabled": false + }, + "public.project_workspace": { + "name": "project_workspace", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "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": { + "project_workspace_workspace_id_unique": { + "name": "project_workspace_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspace_project_id_project_id_fk": { + "name": "project_workspace_project_id_project_id_fk", + "tableFrom": "project_workspace", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_workspace_workspace_id_workspace_id_fk": { + "name": "project_workspace_workspace_id_workspace_id_fk", + "tableFrom": "project_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_workspace_project_id_workspace_id_pk": { + "name": "project_workspace_project_id_workspace_id_pk", + "columns": ["project_id", "workspace_id"] + } + }, + "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 + }, + "primary_provider_id": { + "name": "primary_provider_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": { + "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_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_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 + }, + "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()" + }, + "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'" + }, + "credit_balance": { + "name": "credit_balance", + "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'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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_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_activity_idx": { + "name": "workflow_execution_logs_workspace_activity_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "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 + }, + "fork_sync_new_workflows_excluded": { + "name": "fork_sync_new_workflows_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "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_build": { + "name": "workspace_file_search_build", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_file_search_build_file_idx": { + "name": "workspace_file_search_build_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_build_cleanup_idx": { + "name": "workspace_file_search_build_cleanup_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_build\".\"expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_chunk": { + "name": "workspace_file_search_chunk", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_start": { + "name": "line_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fragment": { + "name": "fragment", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "overlap": { + "name": "overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_chunk_line_idx": { + "name": "workspace_file_search_chunk_line_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "line_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_chunk_content_idx": { + "name": "workspace_file_search_chunk_content_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": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_chunk", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_chunk_pk": { + "name": "workspace_file_search_chunk_pk", + "columns": ["build_id", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_search_chunk_content_size": { + "name": "workspace_file_search_chunk_content_size", + "value": "octet_length(\"workspace_file_search_chunk\".\"content\") <= 8192" + }, + "workspace_file_search_chunk_position": { + "name": "workspace_file_search_chunk_position", + "value": "\"workspace_file_search_chunk\".\"ordinal\" >= 0 AND \"workspace_file_search_chunk\".\"line_start\" > 0 AND \"workspace_file_search_chunk\".\"overlap\" BETWEEN 0 AND 2" + } + }, + "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_revision": { + "name": "workspace_file_search_revision", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "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'" + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": 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 + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "handoff_expires_at": { + "name": "handoff_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_revision_workspace_status_idx": { + "name": "workspace_file_search_revision_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": {} + }, + "workspace_file_search_revision_build_idx": { + "name": "workspace_file_search_revision_build_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_pending_idx": { + "name": "workspace_file_search_revision_pending_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_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_active_idx": { + "name": "workspace_file_search_revision_active_idx", + "columns": [ + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_revision_file_id_workspace_files_id_fk": { + "name": "workspace_file_search_revision_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "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_file_version": { + "name": "workspace_file_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "workspace_file_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_user_ids": { + "name": "author_user_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "restored_from_version": { + "name": "restored_from_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_status": { + "name": "secret_provenance_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_entries": { + "name": "secret_provenance_entries", + "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": { + "workspace_file_version_file_version_unique": { + "name": "workspace_file_version_file_version_unique", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_key_unique": { + "name": "workspace_file_version_key_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_id_idx": { + "name": "workspace_file_version_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_superseded_idx": { + "name": "workspace_file_version_workspace_superseded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "superseded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_version\".\"superseded_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_version_file_id_workspace_files_id_fk": { + "name": "workspace_file_version_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_version_workspace_id_workspace_id_fk": { + "name": "workspace_file_version_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_version_provenance_status_check": { + "name": "workspace_file_version_provenance_status_check", + "value": "\"workspace_file_version\".\"secret_provenance_status\" IS NULL OR \"workspace_file_version\".\"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_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": "date_trunc('milliseconds', 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_workspace_active_keyset_idx": { + "name": "workspace_files_workspace_active_keyset_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "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 + }, + "public.workspace_visit": { + "name": "workspace_visit", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visited_at": { + "name": "visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_visit_workspace_idx": { + "name": "workspace_visit_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_visit_user_id_user_id_fk": { + "name": "workspace_visit_user_id_user_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_visit_workspace_id_workspace_id_fk": { + "name": "workspace_visit_workspace_id_workspace_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_visit_user_id_workspace_id_pk": { + "name": "workspace_visit_user_id_workspace_id_pk", + "columns": ["user_id", "workspace_id"] + } + }, + "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_file_version_source": { + "name": "workspace_file_version_source", + "schema": "public", + "values": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"] + }, + "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 6560207fc2c..d97e885e7ee 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2745,6 +2745,13 @@ "when": 1790723607543, "tag": "0392_dashboard", "breakpoints": true + }, + { + "idx": 393, + "version": "7", + "when": 1790912634834, + "tag": "0393_project_foundation", + "breakpoints": true } ] } diff --git a/packages/db/package.json b/packages/db/package.json index 31cf5145650..9ff079b5d8c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -28,6 +28,10 @@ "./sso-primary-provider": { "types": "./sso-primary-provider.ts", "default": "./sso-primary-provider.ts" + }, + "./project-backfill": { + "types": "./project-backfill.ts", + "default": "./project-backfill.ts" } }, "scripts": { diff --git a/packages/db/project-backfill.ts b/packages/db/project-backfill.ts new file mode 100644 index 00000000000..181cfe17246 --- /dev/null +++ b/packages/db/project-backfill.ts @@ -0,0 +1,471 @@ +import { createHash } from 'node:crypto' +import { createLogger } from '@sim/logger' +import { classifyDatabaseFailure, getPostgresErrorCode } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { backoffWithJitter } from '@sim/utils/retry' +import { compareStrings, truncate } from '@sim/utils/string' +import postgres, { type Sql, type TransactionSql } from 'postgres' + +type BackfillSql = Sql | TransactionSql + +const logger = createLogger('ProjectBackfill', { enabled: true, logLevel: 'INFO' }) +const MAX_FAMILY = 10_000 +const MAX_WORKSPACES = 250_000 +const RUN_LOCK = 'sim-project-backfill-v1' + +interface LegacyWorkspace { + id: string + name: string + organization_id: string | null + owner_id: string + archived_at: string | null + forked_from_workspace_id: string | null +} + +interface FamilyPlan { + rootId: string + projectId: string + workspaceIds: string[] + fingerprint: string + name: string +} + +interface Conflict { + workspaceId: string + projectIds: string[] + workspaceIds: string[] + reason: string +} + +export interface ProjectBackfillReport { + version: 1 + databaseId: string + mode: 'dry-run' | 'apply' | 'verify' + startedAt: string + finishedAt?: string + completed: boolean + ready: boolean + stopped?: string + errorCode?: string + scanned: number + families: number + createdProjects: number + assignedWorkspaces: number + unchangedFamilies: number + retries: number + plan: FamilyPlan[] + conflicts: Conflict[] +} + +interface BackfillOptions { + mode: ProjectBackfillReport['mode'] + databaseId: string + plan?: ProjectBackfillReport + maxFamilySize?: number + seconds?: number + shouldStop?: () => boolean + onProgress?: (report: ProjectBackfillReport) => Promise +} + +function fingerprint(family: LegacyWorkspace[]): string { + return createHash('sha256').update(JSON.stringify(family)).digest('hex') +} + +function conflict( + rootId: string, + reason: string, + ids: string[], + projects: string[] = [] +): Conflict { + return { workspaceId: rootId, reason, workspaceIds: ids, projectIds: projects } +} + +/** Only reads and transactions known to have rolled back may be retried. */ +async function withDatabaseRetry( + operation: () => Promise, + write: boolean, + report: ProjectBackfillReport, + stopped: () => boolean +): Promise { + for (let attempt = 1; ; attempt++) { + try { + return await operation() + } catch (error) { + const code = getPostgresErrorCode(error) + const category = classifyDatabaseFailure(error) + const retryable = write + ? ['40001', '40P01', '55P03'].includes(code ?? '') + : category === 'connection' || ['53300', '57P03'].includes(code ?? '') + if (!retryable || attempt >= 3 || stopped()) throw error + report.retries++ + logger.warn('Retrying Project backfill database operation', { code, write, attempt }) + await sleep(backoffWithJitter(attempt, null, { baseMs: 250, maxMs: 2000 })) + } + } +} + +async function configureTransaction(tx: BackfillSql) { + await tx`SET LOCAL lock_timeout = '2s'` + await tx`SET LOCAL statement_timeout = '30s'` + await tx`SET LOCAL idle_in_transaction_session_timeout = '30s'` + await tx`SET LOCAL transaction_timeout = '30s'` +} + +async function discoverFamily(tx: BackfillSql, rootId: string, maximum: number) { + const family = await tx` + WITH RECURSIVE family AS ( + SELECT id, name, organization_id, owner_id, archived_at::text AS archived_at, forked_from_workspace_id FROM workspace WHERE id = ${rootId} + UNION + SELECT w.id, w.name, w.organization_id, w.owner_id, w.archived_at::text, w.forked_from_workspace_id + FROM workspace w JOIN family f ON w.forked_from_workspace_id = f.id + ) SELECT * FROM family LIMIT ${maximum + 1} + ` + return family.sort((a, b) => compareStrings(a.id, b.id)) +} + +async function inspectFamily(tx: BackfillSql, rootId: string, maximum: number) { + const family = await discoverFamily(tx, rootId, maximum) + const root = family.find((row) => row.id === rootId) + const ids = family.map((row) => row.id) + if (!root || root.forked_from_workspace_id !== null) + return { conflict: conflict(rootId, 'Root missing or now attached to another family', ids) } + if (family.length > maximum) + return { conflict: conflict(rootId, 'Family exceeds batch limit', ids) } + if (family.some((row) => row.organization_id !== root.organization_id)) + return { conflict: conflict(rootId, 'Family spans organizations', ids) } + const memberships = await tx<{ project_id: string }[]>` + SELECT DISTINCT project_id FROM project_workspace WHERE workspace_id = ANY(${ids}::text[]) ORDER BY project_id + ` + const projectIds = memberships.map((row) => row.project_id) + if (projectIds.length > 1) + return { conflict: conflict(rootId, 'Family spans multiple Projects', ids, projectIds) } + const existingId = projectIds[0] + if (existingId) { + const [existing] = await tx<{ organization_id: string | null; archived_at: Date | null }[]>` + SELECT organization_id, archived_at FROM project WHERE id = ${existingId} + ` + if (!existing || existing.organization_id !== root.organization_id) + return { conflict: conflict(rootId, 'Project organization differs', ids, projectIds) } + const outside = + await tx`SELECT workspace_id FROM project_workspace WHERE project_id = ${existingId} AND NOT (workspace_id = ANY(${ids}::text[])) LIMIT 1` + if (outside.length) + return { conflict: conflict(rootId, 'Project includes another lineage', ids, projectIds) } + if (Boolean(existing.archived_at) !== family.every((row) => row.archived_at !== null)) + return { conflict: conflict(rootId, 'Project archive state differs', ids, projectIds) } + } + const [{ missing }] = await tx<{ missing: number }[]>` + SELECT count(*)::int AS missing FROM workspace w WHERE w.id = ANY(${ids}::text[]) + AND NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.workspace_id = w.id) + ` + return { root, family, ids, existingId, missing } +} + +async function applyFamily(tx: BackfillSql, planned: FamilyPlan, maximum: number) { + // NOWAIT prevents waiting behind application writers with an opposite row/advisory lock order. + await tx`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + const state = await inspectFamily(tx, planned.rootId, maximum) + if (state.conflict) return { conflict: state.conflict, created: 0, assigned: 0 } + if (fingerprint(state.family) !== planned.fingerprint) + return { + conflict: conflict( + planned.rootId, + 'Family changed since dry-run; produce a new plan', + state.ids + ), + created: 0, + assigned: 0, + } + if (state.existingId && state.existingId !== planned.projectId) + return { + conflict: conflict(planned.rootId, 'Project assignment changed since dry-run', state.ids, [ + state.existingId, + planned.projectId, + ]), + created: 0, + assigned: 0, + } + if (!state.existingId) { + await tx`INSERT INTO project (id, name, organization_id, owner_id, archived_at) + SELECT ${planned.projectId}, ${planned.name}, ${state.root.organization_id}, ${state.root.owner_id}, + CASE WHEN count(archived_at) = count(*) THEN max(archived_at) ELSE NULL END + FROM workspace WHERE id = ANY(${state.ids}::text[])` + } + const inserted = await tx`INSERT INTO project_workspace (project_id, workspace_id) + SELECT ${planned.projectId}, id FROM workspace WHERE id = ANY(${state.ids}::text[]) + AND NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.workspace_id = workspace.id)` + return { created: state.existingId ? 0 : 1, assigned: inserted.count } +} + +async function inspectGlobalState(tx: BackfillSql) { + const rows = await tx<{ id: string; reason: string }[]>` + SELECT p.id, 'Empty Project' AS reason FROM project p + WHERE NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.project_id = p.id) + UNION ALL + SELECT pw.project_id, 'Orphan membership or organization mismatch' FROM project_workspace pw + LEFT JOIN project p ON p.id = pw.project_id LEFT JOIN workspace w ON w.id = pw.workspace_id + WHERE p.id IS NULL OR w.id IS NULL OR p.organization_id IS DISTINCT FROM w.organization_id + LIMIT 1001 + ` + if (rows.length > 1000) throw new Error('Global anomaly report limit exceeded; repair and rerun') + return rows.map((row) => conflict('', row.reason, [], [row.id])) +} + +/** Validate the operator artifact before it can select any write target. */ +export function readProjectBackfillPlan(value: unknown, databaseId: string): ProjectBackfillReport { + if (!value || typeof value !== 'object') throw new Error('Invalid dry-run artifact') + const plan = value as Partial + if ( + plan.version !== 1 || + plan.mode !== 'dry-run' || + !plan.completed || + plan.databaseId !== databaseId || + !Array.isArray(plan.plan) || + !Array.isArray(plan.conflicts) || + plan.conflicts.length + ) + throw new Error('Apply requires a completed, conflict-free dry-run for this database') + let total = 0 + const roots = new Set() + const projects = new Set() + const members = new Set() + for (const family of plan.plan) { + if ( + !family || + typeof family.rootId !== 'string' || + !family.rootId || + typeof family.projectId !== 'string' || + !family.projectId || + typeof family.fingerprint !== 'string' || + !/^[a-f0-9]{64}$/.test(family.fingerprint) || + typeof family.name !== 'string' || + family.name.trim().length < 1 || + family.name.length > 100 || + !Array.isArray(family.workspaceIds) || + !family.workspaceIds.length || + family.workspaceIds.length > MAX_FAMILY + ) + throw new Error('Malformed family in dry-run artifact') + if (roots.has(family.rootId) || projects.has(family.projectId)) + throw new Error('Duplicate planned root or Project') + roots.add(family.rootId) + projects.add(family.projectId) + for (const id of family.workspaceIds) { + if (typeof id !== 'string' || !id || members.has(id)) + throw new Error('Invalid or repeated planned environment') + members.add(id) + } + if (!family.workspaceIds.includes(family.rootId)) + throw new Error('Root missing from dry-run family') + total += family.workspaceIds.length + if (total > MAX_WORKSPACES) throw new Error('Dry-run artifact exceeds workspace limit') + } + return plan as ProjectBackfillReport +} + +/** Bounded operator backfill. Reports are checkpointed before writes and after each commit. */ +export async function backfillProjects( + sql: Sql, + options: BackfillOptions +): Promise { + const maximum = options.maxFamilySize ?? MAX_FAMILY + const seconds = options.seconds ?? 600 + if ( + !Number.isInteger(maximum) || + maximum < 1 || + maximum > MAX_FAMILY || + !Number.isInteger(seconds) || + seconds < 1 || + seconds > 3600 + ) + throw new Error('Invalid family or runtime limit') + const source = + options.mode === 'apply' ? readProjectBackfillPlan(options.plan, options.databaseId) : null + const report: ProjectBackfillReport = { + version: 1, + databaseId: options.databaseId, + mode: options.mode, + startedAt: new Date().toISOString(), + completed: false, + ready: false, + scanned: 0, + families: 0, + createdProjects: 0, + assignedWorkspaces: 0, + unchangedFamilies: 0, + retries: 0, + plan: [], + conflicts: [], + } + const deadline = Date.now() + seconds * 1000 + const stopped = () => Boolean(options.shouldStop?.()) || Date.now() >= deadline + const checkpoint = async () => { + await options.onProgress?.(report) + } + let lockLost = false + if (sql.options.host.length !== 1 || sql.options.port.length !== 1) + throw new Error('Backfill requires a single direct primary endpoint') + const lockClient = postgres({ + host: sql.options.host[0], + port: sql.options.port[0], + database: sql.options.database, + user: sql.options.user, + password: sql.options.pass ?? undefined, + ssl: sql.options.ssl, + connection: sql.options.connection, + onnotice: sql.options.onnotice, + max: 1, + idle_timeout: 0, + max_lifetime: null, + connect_timeout: 10, + onclose: () => { + lockLost = true + }, + }) + const connection = await lockClient.reserve() + let locked = false + let backendPid = 0 + try { + // Session settings also bound discovery and the first lock attempt. + await connection`SET statement_timeout = '30s'` + await connection`SET lock_timeout = '2s'` + const [identity] = await connection<{ pid: number; locked: boolean }[]>` + SELECT pg_backend_pid() AS pid, pg_try_advisory_lock(hashtextextended(${RUN_LOCK}, 0)) AS locked + ` + locked = identity.locked + backendPid = identity.pid + if (!locked) throw new Error('Another Project backfill run holds the maintenance lock') + const assertLock = async () => { + if (lockLost) throw new Error('Project backfill maintenance lock was lost') + const [state] = await connection<{ held: boolean }[]>` + SELECT pg_backend_pid() = ${backendPid} AND EXISTS ( + SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid() + AND classid = ((hashtextextended(${RUN_LOCK}, 0) >> 32) & 4294967295)::oid + AND objid = (hashtextextended(${RUN_LOCK}, 0) & 4294967295)::oid AND objsubid = 1 AND granted + ) AS held + ` + if (!state.held) throw new Error('Project backfill maintenance lock was lost') + } + const transaction = async ( + work: (tx: BackfillSql) => Promise, + write = false + ): Promise => + withDatabaseRetry( + async () => { + await assertLock() + const result = await sql.begin(async (tx) => { + await configureTransaction(tx) + const value = await work(tx) + await assertLock() + return value + }) + return result as T + }, + write, + report, + stopped + ) + await checkpoint() + if (source) { + for (const family of source.plan) { + if (stopped()) break + const outcome = await transaction((tx) => applyFamily(tx, family, maximum), true) + report.scanned += family.workspaceIds.length + report.families++ + if (outcome.conflict) report.conflicts.push(outcome.conflict) + report.createdProjects += outcome.created + report.assignedWorkspaces += outcome.assigned + if (!outcome.conflict && outcome.assigned === 0) report.unchangedFamilies++ + await checkpoint() + } + report.completed = report.families === source.plan.length + } else { + let afterId = '' + for (;;) { + if (stopped()) break + const page = await transaction( + (tx) => + tx< + { id: string }[] + >`SELECT id FROM workspace WHERE id > ${afterId} ORDER BY id LIMIT 100` + ) + if (!page.length) { + report.completed = true + break + } + for (const candidate of page) { + if (stopped()) break + if (++report.scanned > MAX_WORKSPACES) throw new Error('Workspace scan limit exceeded') + const discovery = await transaction(async (tx) => { + const [root] = await tx<{ id: string; parent: string | null; cycle: boolean }[]>` + WITH RECURSIVE ancestors AS ( + SELECT id, forked_from_workspace_id AS parent, ARRAY[id] AS path, false AS cycle, 0 AS depth FROM workspace WHERE id = ${candidate.id} + UNION ALL + SELECT w.id, w.forked_from_workspace_id, a.path || w.id, w.id = ANY(a.path), a.depth + 1 + FROM workspace w JOIN ancestors a ON w.id = a.parent WHERE NOT a.cycle AND a.depth < 1000 + ) SELECT id, parent, cycle FROM ancestors ORDER BY depth DESC LIMIT 1 + ` + if (!root || root.cycle || root.parent !== null) + return { + conflict: conflict( + candidate.id, + 'Cycle, missing parent, or lineage depth exceeds 1000', + [candidate.id] + ), + } + if (root.id !== candidate.id) return null + return inspectFamily(tx, root.id, maximum) + }) + if (discovery) { + if (discovery.conflict) report.conflicts.push(discovery.conflict) + else { + report.families++ + const suffix = ' - Project' + report.plan.push({ + rootId: candidate.id, + projectId: discovery.existingId ?? generateId(), + workspaceIds: discovery.ids, + fingerprint: fingerprint(discovery.family), + name: `${truncate(discovery.root.name.trim() || 'Untitled', 100 - suffix.length, '')}${suffix}`, + }) + report.createdProjects += discovery.existingId ? 0 : 1 + report.assignedWorkspaces += discovery.missing + if (discovery.missing === 0) report.unchangedFamilies++ + } + } + afterId = candidate.id + } + await checkpoint() + } + if (report.completed) report.conflicts.push(...(await transaction(inspectGlobalState))) + } + report.ready = + options.mode === 'verify' && + report.completed && + !report.conflicts.length && + report.assignedWorkspaces === 0 + if (!report.completed) + report.stopped = 'Interrupted or runtime budget exhausted; rerun from the same plan' + report.finishedAt = new Date().toISOString() + await checkpoint() + return report + } catch (error) { + report.completed = false + report.ready = false + report.errorCode = getPostgresErrorCode(error) + report.stopped = report.errorCode + ? `Database operation failed (${report.errorCode}); reconcile before retrying writes` + : 'Run failed; inspect operator logs and last checkpoint' + report.finishedAt = new Date().toISOString() + await checkpoint() + throw error + } finally { + try { + if (locked && !lockLost) + await connection`SELECT pg_advisory_unlock(hashtextextended(${RUN_LOCK}, 0))` + } finally { + connection.release() + await lockClient.end({ timeout: 2 }) + } + } +} diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 23283468902..ad9cf477872 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2058,6 +2058,55 @@ export const workspace = pgTable( }) ) +/** Stable owner of environments and project-wide resources, independent of fork lineage. */ +export const project = pgTable( + 'project', + { + id: text('id').primaryKey(), + name: text('name').notNull(), + organizationId: text('organization_id').references(() => organization.id, { + onDelete: 'restrict', + }), + /** Lifecycle owner for personal and organization Projects; never an implicit access grant. */ + ownerId: text('owner_id') + .notNull() + .references(() => user.id, { onDelete: 'restrict' }), + archivedAt: timestamp('archived_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + nameLength: check( + 'project_name_length', + sql`char_length(btrim(${table.name})) BETWEEN 1 AND 100` + ), + organizationIdx: index('project_organization_archive_id_idx').on( + table.organizationId, + table.archivedAt, + table.id + ), + ownerIdx: index('project_owner_archive_id_idx').on(table.ownerId, table.archivedAt, table.id), + }) +) + +// contract-pending(after project writers are fully deployed and backfill validates): enforce exactly-one membership and active Project environment minimums at commit. +export const projectWorkspace = pgTable( + 'project_workspace', + { + projectId: text('project_id') + .notNull() + .references(() => project.id, { onDelete: 'restrict' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + pk: primaryKey({ columns: [table.projectId, table.workspaceId] }), + workspaceUnique: uniqueIndex('project_workspace_workspace_id_unique').on(table.workspaceId), + }) +) + export const workspaceForkResourceTypeEnum = pgEnum('workspace_fork_resource_type', [ 'workflow', 'oauth_credential', diff --git a/packages/db/scripts/backfill-projects.ts b/packages/db/scripts/backfill-projects.ts new file mode 100644 index 00000000000..1512838d2d5 --- /dev/null +++ b/packages/db/scripts/backfill-projects.ts @@ -0,0 +1,168 @@ +#!/usr/bin/env bun + +import { createHash } from 'node:crypto' +import { readFile, rename, stat, writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { parseArgs } from 'node:util' +import { backfillProjects, readProjectBackfillPlan } from '@sim/db/project-backfill' +import { createLogger } from '@sim/logger' +import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' +import postgres from 'postgres' + +const logger = createLogger('ProjectBackfillCommand', { enabled: true, logLevel: 'INFO' }) +const HELP = `Operator-only Project backfill. Deployment only creates the additive tables. + +bun --no-env-file packages/db/scripts/backfill-projects.ts [options] + +Commands: + identity Print the non-secret database fingerprint; no database writes. + dry-run Discover and validate families; write the plan/report. + apply Apply --from-file PLAN from a completed, conflict-free dry-run. + verify Independently reconcile all environments and Projects; nonzero unless ready. + +Required environment: PROJECT_BACKFILL_DATABASE_URL (direct primary connection). +Reports: PROJECT_BACKFILL_REPORT_PATH (required except identity). +Apply requires --writers-drained and --database-id FINGERPRINT. +Options: --from-file PATH, --seconds 600 (1–3600), --max-family-size 1000 (1–10000). +Run with compatible writers deployed. Pause lineage/ownership writes during apply. +Short NOWAIT table locks exclude concurrent changes during each family transaction. +Interrupted/failed apply: inspect the report, then rerun the SAME dry-run artifact. +Connection loss during writes is never automatically retried; verify the committed state. +No run enables Project consumers or installs the deferred contract constraints.` + +async function main() { + const { positionals, values } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, + options: { + help: { type: 'boolean' }, + 'from-file': { type: 'string' }, + 'writers-drained': { type: 'boolean' }, + 'database-id': { type: 'string' }, + seconds: { type: 'string', default: '600' }, + 'max-family-size': { type: 'string', default: '1000' }, + }, + }) + if (values.help) { + process.stdout.write(`${HELP}\n`) + return + } + const mode = positionals[0] ?? 'dry-run' + if (positionals.length > 1 || !['identity', 'dry-run', 'apply', 'verify'].includes(mode)) + throw new Error('Unknown command; use --help') + const rawUrl = process.env.PROJECT_BACKFILL_DATABASE_URL + if (!rawUrl) + throw new Error('Set PROJECT_BACKFILL_DATABASE_URL explicitly; no application DSN fallback') + const url = new URL(rawUrl) + if ( + !['postgres:', 'postgresql:'].includes(url.protocol) || + !url.hostname || + !url.username || + url.pathname.length < 2 + ) + throw new Error('Expected a direct PostgreSQL database URL') + const databaseId = createHash('sha256') + .update( + JSON.stringify([ + url.hostname.toLowerCase(), + url.port || '5432', + decodeURIComponent(url.pathname), + ]) + ) + .digest('hex') + if (mode === 'identity') { + process.stdout.write(`${databaseId}\n`) + return + } + if (mode !== 'dry-run' && mode !== 'apply' && mode !== 'verify') + throw new Error('Invalid command') + const reportPath = process.env.PROJECT_BACKFILL_REPORT_PATH + if (!reportPath) throw new Error('Set PROJECT_BACKFILL_REPORT_PATH explicitly') + if ( + mode === 'apply' && + (!values['writers-drained'] || values['database-id'] !== databaseId || !values['from-file']) + ) + throw new Error('Apply requires --writers-drained, matching --database-id, and --from-file') + if (mode !== 'apply' && (values['writers-drained'] || values['from-file'])) + throw new Error('Apply-only options used with read-only mode') + const inputPath = values['from-file'] + if (inputPath && resolve(inputPath) === resolve(reportPath)) + throw new Error('Keep the dry-run plan and apply report in separate files') + const plan = inputPath + ? await (async () => { + if ((await stat(inputPath)).size > 32 * 1024 * 1024) + throw new Error('Dry-run artifact exceeds 32 MiB') + return readProjectBackfillPlan(JSON.parse(await readFile(inputPath, 'utf8')), databaseId) + })() + : undefined + const sql = postgres(rawUrl, { + max: 1, + prepare: false, + connect_timeout: 10, + idle_timeout: 0, + max_lifetime: null, + connection: { + application_name: 'sim-project-backfill', + statement_timeout: 30_000, + lock_timeout: 2000, + }, + }) + let interrupted = false + const stop = () => { + interrupted = true + } + process.on('SIGINT', stop) + process.on('SIGTERM', stop) + try { + // Preflight the report destination before opening a transaction that can write. + const partialPath = `${reportPath}.${process.pid}.partial` + await writeFile( + partialPath, + JSON.stringify({ databaseId, mode, completed: false, ready: false, stopped: 'Starting' }), + { mode: 0o600 } + ) + await rename(partialPath, reportPath) + const report = await backfillProjects(sql, { + mode, + databaseId, + plan, + seconds: Number(values.seconds), + maxFamilySize: Number(values['max-family-size']), + shouldStop: () => interrupted, + onProgress: async (progress) => { + await writeFile(partialPath, JSON.stringify(progress, null, 2), { mode: 0o600 }) + await rename(partialPath, reportPath) + logger.info('Project backfill progress', { + mode, + databaseId, + scanned: progress.scanned, + families: progress.families, + createdProjects: progress.createdProjects, + assignedWorkspaces: progress.assignedWorkspaces, + conflicts: progress.conflicts.length, + retries: progress.retries, + completed: progress.completed, + }) + }, + }) + if (!report.completed || report.conflicts.length || (mode === 'verify' && !report.ready)) + process.exitCode = 1 + } finally { + process.off('SIGINT', stop) + process.off('SIGTERM', stop) + await sql.end({ timeout: 5 }) + } +} + +try { + await main() +} catch (error) { + const code = getPostgresErrorCode(error) + // Database messages can contain connection details or row values; retain SQLSTATE, not the payload. + logger.error('Project backfill failed', { + error: code + ? `Database error ${code}; inspect the report and reconcile before retrying` + : getErrorMessage(error), + }) + process.exitCode = 1 +} diff --git a/packages/db/scripts/project-backfill.integration.ts b/packages/db/scripts/project-backfill.integration.ts new file mode 100644 index 00000000000..b60b960c7da --- /dev/null +++ b/packages/db/scripts/project-backfill.integration.ts @@ -0,0 +1,396 @@ +import { spawn } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { backfillProjects, type ProjectBackfillReport } from '@sim/db/project-backfill' +import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { generateId } from '@sim/utils/id' +import postgres, { type Sql } from 'postgres' +import { describe, expect, it } from 'vitest' + +/** Isolated pre-Project fixture; applies the actual additive migration, never a schema mock. */ +async function withDatabase(run: (sql: Sql, url: string) => Promise) { + const admin = postgres(readTestDatabaseUrl(), { max: 1 }) + const database = `project_backfill_test_${generateId().replaceAll('-', '')}` + const url = new URL(readTestDatabaseUrl()) + url.pathname = `/${database}` + await admin.unsafe(`CREATE DATABASE "${database}"`) + const sql = postgres(url.toString(), { max: 1, onnotice: () => undefined }) + try { + await sql.unsafe(` + CREATE TABLE "user" (id text PRIMARY KEY); + CREATE TABLE organization (id text PRIMARY KEY); + CREATE TABLE workspace ( + id text PRIMARY KEY, name text NOT NULL, owner_id text NOT NULL, + organization_id text, archived_at timestamp, forked_from_workspace_id text + ); + INSERT INTO "user" VALUES ('owner'), ('other-owner'); + INSERT INTO organization VALUES ('org-a'), ('org-b'); + `) + await sql.unsafe( + await readFile(new URL('../migrations/0393_project_foundation.sql', import.meta.url), 'utf8') + ) + await run(sql, url.toString()) + } finally { + await sql.end({ timeout: 2 }) + await admin.unsafe(`DROP DATABASE "${database}" WITH (FORCE)`) + await admin.end() + } +} + +async function seed(sql: Sql) { + await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id, archived_at) VALUES + ('a-root', 'Production', 'owner', 'org-a', NULL, NULL), + ('b-child', 'Staging', 'other-owner', 'org-a', 'a-root', NULL), + ('c-grandchild', 'Dev', 'owner', 'org-a', 'b-child', NULL), + ('d-detached', 'Staging', 'owner', NULL, NULL, NULL), + ('e-detached-child', 'Dev', 'owner', NULL, 'd-detached', NULL), + ('f-archived', 'Archive', 'owner', NULL, NULL, '2025-01-01'), + ('g-archived-child', 'Archive child', 'owner', NULL, 'f-archived', '2025-02-01')` +} + +const databaseId = 'local-test-database' +const dryRun = (sql: Sql) => backfillProjects(sql, { mode: 'dry-run', databaseId }) +const verify = (sql: Sql) => backfillProjects(sql, { mode: 'verify', databaseId }) +const apply = (sql: Sql, plan: ProjectBackfillReport) => + backfillProjects(sql, { mode: 'apply', databaseId, plan }) + +describe('Project backfill operator lifecycle', () => { + it('plans roots and detached families, retains archive timestamps, and replays fixed IDs', async () => { + await withDatabase(async (sql) => { + await seed(sql) + const plan = await dryRun(sql) + expect(plan.createdProjects).toBe(3) + expect(plan.assignedWorkspaces).toBe(7) + expect(await sql`SELECT * FROM project`).toHaveLength(0) + expect((await verify(sql)).ready).toBe(false) + const first = await apply(sql, plan) + expect(first.assignedWorkspaces).toBe(7) + expect((await verify(sql)).ready).toBe(true) + const rows = await sql`SELECT id, name, owner_id, archived_at FROM project ORDER BY name` + expect(rows.map((row) => row.name)).toEqual([ + 'Archive - Project', + 'Production - Project', + 'Staging - Project', + ]) + expect( + ( + await sql`SELECT archived_at::text AS value FROM project WHERE name = 'Archive - Project'` + )[0].value + ).toBe('2025-02-01 00:00:00') + expect(rows[1].owner_id).toBe('owner') + const repeat = await apply(sql, plan) + expect(repeat.createdProjects).toBe(0) + expect(repeat.unchangedFamilies).toBe(3) + expect(await sql`SELECT id, name, owner_id, archived_at FROM project ORDER BY name`).toEqual( + rows + ) + }) + }) + + it('completes partial assignments but blocks split Projects, cycles, missing parents and mixed organizations', async () => { + await withDatabase(async (sql) => { + await seed(sql) + await sql`INSERT INTO project (id, name, owner_id, organization_id) VALUES ('existing', 'Keep this name', 'owner', 'org-a')` + await sql`INSERT INTO project_workspace VALUES ('existing', 'a-root', now())` + const plan = await dryRun(sql) + await apply(sql, plan) + expect( + await sql`SELECT workspace_id FROM project_workspace WHERE project_id = 'existing'` + ).toHaveLength(3) + expect((await sql`SELECT name FROM project WHERE id = 'existing'`)[0].name).toBe( + 'Keep this name' + ) + await sql`INSERT INTO project (id, name, owner_id, organization_id) VALUES ('split', 'Split', 'owner', 'org-a'), ('empty', 'Empty', 'owner', NULL)` + await sql`UPDATE project_workspace SET project_id = 'split' WHERE workspace_id = 'b-child'` + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) VALUES ('cycle-a', 'A', 'owner', 'cycle-b'), ('cycle-b', 'B', 'owner', 'cycle-a'), ('orphan', 'Orphan', 'owner', 'missing')` + await sql`UPDATE workspace SET organization_id = 'org-b' WHERE id = 'e-detached-child'` + const broken = await verify(sql) + expect(broken.ready).toBe(false) + expect(broken.conflicts.map((row) => row.reason)).toEqual( + expect.arrayContaining([ + 'Family spans multiple Projects', + 'Family spans organizations', + 'Empty Project', + 'Cycle, missing parent, or lineage depth exceeds 1000', + ]) + ) + expect(broken.conflicts.find((row) => row.workspaceId === 'a-root')?.projectIds).toEqual([ + 'existing', + 'split', + ]) + await expect(apply(sql, { ...broken, mode: 'dry-run' })).rejects.toThrow('conflict-free') + }) + }) + + it('rejects a new descendant added after dry-run without assigning a partial family', async () => { + await withDatabase(async (sql) => { + await seed(sql) + const plan = await dryRun(sql) + await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id) VALUES ('late-child', 'Late', 'owner', 'org-a', 'b-child')` + const result = await apply(sql, plan) + expect( + result.conflicts.some( + (row) => row.workspaceId === 'a-root' && row.reason.includes('changed since dry-run') + ) + ).toBe(true) + expect( + await sql`SELECT * FROM project_workspace WHERE workspace_id IN ('a-root', 'b-child', 'c-grandchild', 'late-child')` + ).toHaveLength(0) + const fresh = await dryRun(sql) + await apply(sql, fresh) + expect((await verify(sql)).ready).toBe(true) + }) + }) + + it('resumes after a committed-family interruption and retains progress when reporting fails', async () => { + await withDatabase(async (sql) => { + await seed(sql) + const plan = await dryRun(sql) + let stop = false + const partial = await backfillProjects(sql, { + mode: 'apply', + databaseId, + plan, + shouldStop: () => stop, + onProgress: async (report) => { + if (report.families === 1) stop = true + }, + }) + expect(partial.completed).toBe(false) + expect(partial.createdProjects).toBe(1) + const firstId = (await sql`SELECT id FROM project`)[0].id + await expect( + backfillProjects(sql, { + mode: 'apply', + databaseId, + plan, + onProgress: async (report) => { + if (report.createdProjects > 0) throw new Error('Report disk unavailable') + }, + }) + ).rejects.toThrow('Report disk unavailable') + expect(await sql`SELECT id FROM project`).toHaveLength(2) + await apply(sql, plan) + expect(await sql`SELECT id FROM project WHERE id = ${firstId}`).toHaveLength(1) + expect((await verify(sql)).ready).toBe(true) + }) + }) + + it('refuses another runner while a run owns the lock and stops on a lost database session', async () => { + await withDatabase(async (sql, url) => { + await seed(sql) + const other = postgres(url, { max: 1 }) + try { + let checked = false + await backfillProjects(sql, { + mode: 'dry-run', + databaseId, + onProgress: async () => { + if (checked) return + checked = true + await expect(dryRun(other)).rejects.toThrow('maintenance lock') + }, + }) + const plan = await dryRun(sql) + let terminated = false + let snapshot: ProjectBackfillReport | undefined + await expect( + backfillProjects(sql, { + mode: 'apply', + databaseId, + plan, + onProgress: async (report) => { + snapshot = structuredClone(report) + if (terminated || report.families !== 1) return + terminated = true + await other`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()` + }, + }) + ).rejects.toThrow() + expect(snapshot?.completed).toBe(false) + expect(snapshot?.createdProjects).toBe(1) + await apply(other, plan) + expect((await verify(other)).ready).toBe(true) + } finally { + await other.end({ timeout: 2 }) + } + }) + }) + + it('fails boundedly behind a live writer, then succeeds after the writer finishes', async () => { + await withDatabase(async (sql, url) => { + await seed(sql) + const plan = await dryRun(sql) + const writer = postgres(url, { max: 1 }) + try { + await writer.begin(async (tx) => { + await tx`UPDATE workspace SET name = name WHERE id = 'a-root'` + let last: ProjectBackfillReport | undefined + await expect( + backfillProjects(sql, { + mode: 'apply', + databaseId, + plan, + onProgress: async (report) => { + last = structuredClone(report) + }, + }) + ).rejects.toMatchObject({ code: '55P03' }) + expect(last?.retries).toBe(2) + expect(last?.createdProjects).toBe(0) + }) + await apply(sql, plan) + expect((await verify(sql)).ready).toBe(true) + } finally { + await writer.end() + } + }) + }) + + it('retries rolled-back serialization failures but stops on cancellation without partial writes', async () => { + await withDatabase(async (sql) => { + await seed(sql) + const plan = await dryRun(sql) + await sql.unsafe(` + CREATE SEQUENCE attempts; + CREATE FUNCTION fail_twice() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF nextval('attempts') <= 2 THEN RAISE EXCEPTION 'injected serialization failure' USING ERRCODE = '40001'; END IF; + RETURN NEW; + END $$; + CREATE TRIGGER fail_twice BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION fail_twice(); + `) + const result = await apply(sql, plan) + expect(result.retries).toBe(2) + expect(result.createdProjects).toBe(3) + expect((await verify(sql)).ready).toBe(true) + await sql`DELETE FROM project_workspace` + await sql`DELETE FROM project` + await sql.unsafe(` + DROP TRIGGER fail_twice ON project; + CREATE FUNCTION cancel_write() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN RAISE EXCEPTION 'injected cancellation' USING ERRCODE = '57014'; END $$; + CREATE TRIGGER cancel_write BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION cancel_write(); + `) + let report: ProjectBackfillReport | undefined + await expect( + backfillProjects(sql, { + mode: 'apply', + databaseId, + plan, + onProgress: async (progress) => { + report = structuredClone(progress) + }, + }) + ).rejects.toMatchObject({ code: '57014' }) + expect(report?.retries).toBe(0) + expect(report?.errorCode).toBe('57014') + expect(await sql`SELECT * FROM project_workspace`).toHaveLength(0) + expect(await sql`SELECT * FROM project`).toHaveLength(0) + }) + }) + + it('fails readiness for empty Projects and archived-state mismatch without mutating them', async () => { + await withDatabase(async (sql) => { + await seed(sql) + await apply(sql, await dryRun(sql)) + await sql`UPDATE project SET archived_at = now() WHERE name = 'Production - Project'` + await sql`INSERT INTO project (id, name, owner_id) VALUES ('empty', 'Empty', 'owner')` + const result = await verify(sql) + expect(result.ready).toBe(false) + expect(result.conflicts.map((row) => row.reason)).toEqual( + expect.arrayContaining(['Empty Project', 'Project archive state differs']) + ) + expect(await sql`SELECT id FROM project WHERE id = 'empty'`).toHaveLength(1) + }) + }) +}) + +/** CLI cases exercise the normal connection contract against an isolated database. */ +describe('Project backfill CLI', () => { + it('runs dry-run/apply/verify with durable reports and refuses a mismatched target', async () => { + const { mkdtemp, rm } = await import('node:fs/promises') + const { tmpdir } = await import('node:os') + const { join } = await import('node:path') + await withDatabase(async (sql, url) => { + await seed(sql) + const directory = await mkdtemp(join(tmpdir(), 'project-backfill-test-')) + const planPath = join(directory, 'plan.json') + const reportPath = join(directory, 'report.json') + const run = async (args: string[], report = reportPath) => { + const child = spawn('bun', ['--no-env-file', 'scripts/backfill-projects.ts', ...args], { + env: { + ...process.env, + PROJECT_BACKFILL_DATABASE_URL: url, + PROJECT_BACKFILL_REPORT_PATH: report, + }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8').on('data', (chunk: string) => { + stdout += chunk + }) + child.stderr.setEncoding('utf8').on('data', (chunk: string) => { + stderr += chunk + }) + const code = await new Promise((resolve, reject) => { + child.on('error', reject) + child.on('close', resolve) + }) + expect(stdout + stderr).not.toContain(url) + return { stdout, stderr, code } + } + try { + const dry = await run(['dry-run'], planPath) + expect(dry.code, dry.stdout + dry.stderr).toBe(0) + const plan = JSON.parse(await readFile(planPath, 'utf8')) as ProjectBackfillReport + expect( + ( + await run([ + 'apply', + '--from-file', + planPath, + '--writers-drained', + '--database-id', + 'wrong', + ]) + ).code + ).toBe(1) + expect(await sql`SELECT * FROM project`).toHaveLength(0) + expect( + ( + await run([ + 'apply', + '--from-file', + planPath, + '--writers-drained', + '--database-id', + plan.databaseId, + ]) + ).code + ).toBe(0) + expect((await run(['verify'])).code).toBe(0) + const report = JSON.parse(await readFile(reportPath, 'utf8')) as ProjectBackfillReport + expect(report.ready).toBe(true) + expect( + ( + await run( + [ + 'apply', + '--from-file', + planPath, + '--writers-drained', + '--database-id', + plan.databaseId, + ], + planPath + ) + ).code + ).toBe(1) + expect(JSON.parse(await readFile(planPath, 'utf8')).mode).toBe('dry-run') + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }) +}) diff --git a/packages/sim-cli/src/generated/v2-api.ts b/packages/sim-cli/src/generated/v2-api.ts index 53a4b04bd42..0dbe9171714 100644 --- a/packages/sim-cli/src/generated/v2-api.ts +++ b/packages/sim-cli/src/generated/v2-api.ts @@ -2589,6 +2589,7 @@ export type CreatePermissionGroupBody = { hideSandboxesTab?: boolean disableOAuthAppAccess?: boolean disableKnowledgeBaseExport?: boolean + deniedPartialAccessProjectIssues?: Array } isDefault?: boolean workspaceIds?: Array @@ -2642,6 +2643,7 @@ type CreatePermissionGroupResponseRef0 = { hideSandboxesTab: boolean disableOAuthAppAccess: boolean disableKnowledgeBaseExport: boolean + deniedPartialAccessProjectIssues: Array } isDefault: boolean membershipMode: string @@ -6127,6 +6129,7 @@ type GetPermissionGroupResponseRef0 = { hideSandboxesTab: boolean disableOAuthAppAccess: boolean disableKnowledgeBaseExport: boolean + deniedPartialAccessProjectIssues: Array } isDefault: boolean membershipMode: string @@ -7274,6 +7277,7 @@ type GetWorkspacePermissionConfigResponseRef0 = { hideSandboxesTab: boolean disableOAuthAppAccess: boolean disableKnowledgeBaseExport: boolean + deniedPartialAccessProjectIssues: Array } | null entitled: boolean organizationId: string | null @@ -8951,6 +8955,7 @@ type ListPermissionGroupsResponseRef0 = { hideSandboxesTab: boolean disableOAuthAppAccess: boolean disableKnowledgeBaseExport: boolean + deniedPartialAccessProjectIssues: Array } isDefault: boolean membershipMode: string @@ -10049,6 +10054,7 @@ type PreviewOrganizationAccessRequestResponseRef0 = { | 'hideSandboxesTab' | 'disableOAuthAppAccess' | 'disableKnowledgeBaseExport' + | 'deniedPartialAccessProjectIssues' label: string before: boolean | Array | null after: boolean | Array | null @@ -10205,6 +10211,7 @@ type PreviewOrganizationAccessRequestResponseRef1 = { | 'hideSandboxesTab' | 'disableOAuthAppAccess' | 'disableKnowledgeBaseExport' + | 'deniedPartialAccessProjectIssues' label: string before: boolean | Array | null after: boolean | Array | null @@ -13195,6 +13202,7 @@ export type UpdatePermissionGroupBody = { hideSandboxesTab?: boolean disableOAuthAppAccess?: boolean disableKnowledgeBaseExport?: boolean + deniedPartialAccessProjectIssues?: Array } isDefault?: boolean workspaceIds?: Array @@ -13248,6 +13256,7 @@ type UpdatePermissionGroupResponseRef0 = { hideSandboxesTab: boolean disableOAuthAppAccess: boolean disableKnowledgeBaseExport: boolean + deniedPartialAccessProjectIssues: Array } isDefault: boolean membershipMode: string diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index c13f3a238c3..fb2e51d7ce4 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -501,6 +501,8 @@ export const GENERATED_SCHEMA_TABLES = { 'createdAt', 'updatedAt', ], + project: ['id', 'name', 'organizationId', 'ownerId', 'archivedAt', 'createdAt', 'updatedAt'], + projectWorkspace: ['projectId', 'workspaceId', 'createdAt'], workspaceForkResourceMap: [ 'id', 'childWorkspaceId', From aa9b41577f456b232b74c2a013c92a6cb00f5b27 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 01:35:39 -0700 Subject: [PATCH 2/8] feat(projects): create projects with their initial environment --- apps/sim/app/api/projects/route.ts | 14 +- apps/sim/lib/api/contracts/projects.ts | 15 ++ .../__integration__/foundation.integration.ts | 172 +++++++++++++++++- .../lib/projects/application/authorization.ts | 2 +- .../projects/application/create-project.ts | 126 +++++++++++++ apps/sim/lib/projects/application/index.ts | 1 + .../lib/projects/application/operations.ts | 7 +- apps/sim/lib/projects/create-input.ts | 8 + apps/sim/lib/projects/membership.ts | 3 +- apps/sim/lib/workspaces/create.ts | 39 ++-- docs/plans/project-entity-foundation.md | 5 +- packages/audit/src/types.ts | 1 + 12 files changed, 371 insertions(+), 22 deletions(-) create mode 100644 apps/sim/lib/projects/application/create-project.ts create mode 100644 apps/sim/lib/projects/create-input.ts diff --git a/apps/sim/app/api/projects/route.ts b/apps/sim/app/api/projects/route.ts index fe0aca010ca..852cc6f3a4c 100644 --- a/apps/sim/app/api/projects/route.ts +++ b/apps/sim/app/api/projects/route.ts @@ -1,11 +1,11 @@ -import { listProjectsContract } from '@/lib/api/contracts/projects' +import { createProjectContract, listProjectsContract } from '@/lib/api/contracts/projects' import { defineInternalJsonRoute, internalOrchestrationErrorPolicy, internalRateLimits, internalSessionAuth, } from '@/lib/api/server/routes' -import { listProjects, projectOperations } from '@/lib/projects/application' +import { createProject, listProjects, projectOperations } from '@/lib/projects/application' export const GET = defineInternalJsonRoute({ contract: listProjectsContract, @@ -16,3 +16,13 @@ export const GET = defineInternalJsonRoute({ mapInput: ({ query }) => query, useCase: listProjects, }) + +export const POST = defineInternalJsonRoute({ + contract: createProjectContract, + auth: internalSessionAuth, + operation: projectOperations.create, + rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ body }) => body, + useCase: createProject, +}) diff --git a/apps/sim/lib/api/contracts/projects.ts b/apps/sim/lib/api/contracts/projects.ts index 85e774a9cd6..f25549855e0 100644 --- a/apps/sim/lib/api/contracts/projects.ts +++ b/apps/sim/lib/api/contracts/projects.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { nonEmptyIdSchema } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' +import { createProjectInputSchema } from '@/lib/projects/create-input' export const projectEnvironmentSchema = z.object({ id: nonEmptyIdSchema, @@ -84,3 +85,17 @@ export const getWorkspaceProjectContract = defineRouteContract({ params: workspaceProjectParamsSchema, response: { mode: 'json', schema: getProjectResponseSchema }, }) + +export const createProjectBodySchema = createProjectInputSchema +export type CreateProjectBody = z.input +export const createProjectResponseSchema = z.object({ + project: z.object({ id: nonEmptyIdSchema, name: z.string() }), + initialEnvironment: z.object({ id: nonEmptyIdSchema, name: z.string() }), +}) +export type CreateProjectResponse = z.output +export const createProjectContract = defineRouteContract({ + method: 'POST', + path: '/api/projects', + body: createProjectBodySchema, + response: { mode: 'json', status: 201, schema: createProjectResponseSchema }, +}) diff --git a/apps/sim/lib/projects/__integration__/foundation.integration.ts b/apps/sim/lib/projects/__integration__/foundation.integration.ts index 0d8cdd291d5..1d617d62d19 100644 --- a/apps/sim/lib/projects/__integration__/foundation.integration.ts +++ b/apps/sim/lib/projects/__integration__/foundation.integration.ts @@ -11,6 +11,7 @@ import { user, userStats, workflow, + workflowBlocks, workspace, workspaceForkBlockMap, workspaceForkDependentValue, @@ -21,14 +22,15 @@ import { createSessionPrincipal, createWorkspaceApiKeyPrincipal, } from '@sim/testing/factories/principal.factory' -import { getErrorMessage } from '@sim/utils/errors' +import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, eq, inArray } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, describe, expect, it } from 'vitest' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion' import { archiveProject, + createProject, getProject, getProjectIssueAccess, getWorkspaceProject, @@ -180,6 +182,172 @@ afterAll(async () => { }) describe('Project foundation at the database and application boundary', () => { + check( + 'Project creation commits its named first environment and starter workflow in the requested scope', + async () => { + for (const personal of [false, true]) { + const f = await fixture(true, 1) + const organizationId = personal ? null : f.organizationId + if (!f.organizationId) throw new Error('Missing organization fixture') + await db.insert(member).values({ + id: generateId(), + organizationId: f.organizationId, + userId: f.teammateId, + role: 'admin', + createdAt: new Date(), + }) + const result = await createProject.execute({ + principal: f.teammate, + input: { + organizationId, + name: 'Customer support', + initialEnvironment: { name: 'Production' }, + }, + request, + }) + environments.push(result.initialEnvironment.id) + const [created] = await db.select().from(project).where(eq(project.id, result.project.id)) + expect(created).toMatchObject({ + name: 'Customer support', + organizationId, + ownerId: f.teammateId, + }) + const [env] = await db + .select() + .from(workspace) + .where(eq(workspace.id, result.initialEnvironment.id)) + expect(env).toMatchObject({ + name: 'Production', + organizationId, + ownerId: f.teammateId, + billedAccountUserId: personal ? f.teammateId : f.ownerId, + }) + expect( + await db + .select({ + projectId: projectWorkspace.projectId, + workspaceId: projectWorkspace.workspaceId, + }) + .from(projectWorkspace) + .where(eq(projectWorkspace.projectId, created.id)) + ).toEqual([{ projectId: created.id, workspaceId: env.id }]) + const grants = await db.select().from(permissions).where(eq(permissions.entityId, env.id)) + expect( + grants.map((grant) => ({ userId: grant.userId, permissionType: grant.permissionType })) + ).toEqual( + expect.arrayContaining([ + { userId: f.teammateId, permissionType: 'admin' }, + ...(personal ? [] : [{ userId: f.ownerId, permissionType: 'admin' }]), + ]) + ) + expect(grants).toHaveLength(personal ? 1 : 2) + const workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, env.id)) + expect(workflows).toHaveLength(1) + const blocks = await db + .select() + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflows[0].id)) + expect(blocks.length).toBeGreaterThan(0) + } + } + ) + + check( + 'Project creation refuses workspace keys and foreign organizations without creating resources', + async () => { + const f = await fixture(true, 1) + const foreign = await fixture(true, 1) + const input = { + organizationId: foreign.organizationId, + name: 'Forbidden project', + initialEnvironment: { name: 'Production' }, + } + await expect( + createProject.execute({ principal: f.owner, input, request }) + ).rejects.toMatchObject({ code: 'forbidden' }) + await expect( + createProject.execute({ + principal: createWorkspaceApiKeyPrincipal({ workspaceId: f.ids[0] }), + input: { ...input, organizationId: f.organizationId }, + request, + }) + ).rejects.toThrow('cannot perform operation') + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(1) + expect( + await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) + ).toHaveLength(1) + } + ) + + check( + 'Project creation cannot bypass the workspace-creation restriction through personal scope', + async () => { + const f = await fixture(true, 1) + if (!f.organizationId) throw new Error('Missing organization fixture') + await db.insert(permissionGroup).values({ + id: generateId(), + organizationId: f.organizationId, + name: 'Creation disabled', + createdBy: f.ownerId, + isDefault: true, + config: { disableWorkspaceCreation: true }, + }) + for (const organizationId of [f.organizationId, null]) { + await expect( + createProject.execute({ + principal: f.owner, + input: { + organizationId, + name: 'Forbidden project', + initialEnvironment: { name: 'Production' }, + }, + request, + }) + ).rejects.toMatchObject({ detailCode: 'PERMISSION_GROUP_CAPABILITY_BLOCKED' }) + } + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(1) + expect( + await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) + ).toHaveLength(1) + } + ) + + check( + 'A starter-workflow failure rolls back the Project, environment and permissions together', + async () => { + const f = await fixture(false, 1) + const constraint = sql.identifier(`project_create_test_${generateId().replaceAll('-', '')}`) + const before = await db.select().from(permissions).where(eq(permissions.userId, f.ownerId)) + await db.execute( + sql`ALTER TABLE ${workflow} ADD CONSTRAINT ${constraint} CHECK (${workflow.userId} <> ${f.ownerId}) NOT VALID`.inlineParams() + ) + try { + await expect( + createProject.execute({ + principal: f.owner, + input: { + organizationId: null, + name: 'Rollback project', + initialEnvironment: { name: 'Rollback environment' }, + }, + request, + }) + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23514') + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength( + 1 + ) + expect( + await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) + ).toHaveLength(1) + expect( + await db.select().from(permissions).where(eq(permissions.userId, f.ownerId)) + ).toEqual(before) + } finally { + await db.execute(sql`ALTER TABLE ${workflow} DROP CONSTRAINT ${constraint}`) + } + } + ) + check('workspace creation and forks commit exactly one Project membership', async () => { const f = await fixture(false, 1) const source = await db.transaction((tx) => diff --git a/apps/sim/lib/projects/application/authorization.ts b/apps/sim/lib/projects/application/authorization.ts index a0cf6b571a4..47ebc4f7b43 100644 --- a/apps/sim/lib/projects/application/authorization.ts +++ b/apps/sim/lib/projects/application/authorization.ts @@ -13,7 +13,7 @@ import { lockProject } from '@/lib/projects/membership' export function requireProjectPrincipal( principal: Principal, - operation: ProjectOperation + operation: Pick ): asserts principal is SessionPrincipal { if (principal.kind !== 'session') throw new PrincipalKindAuthorizationError(principal.kind, operation.id) diff --git a/apps/sim/lib/projects/application/create-project.ts b/apps/sim/lib/projects/application/create-project.ts new file mode 100644 index 00000000000..dec1a3ea376 --- /dev/null +++ b/apps/sim/lib/projects/application/create-project.ts @@ -0,0 +1,126 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { db } from '@sim/db' +import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' +import { recordProjectedUseCaseAuditEntries } from '@/lib/core/application/authorized-workspace-use-case' +import type { OperationUseCase } from '@/lib/core/application/operation' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { refuseCapability } from '@/lib/permission-groups/capabilities' +import { requireProjectPrincipal } from '@/lib/projects/application/authorization' +import { projectOperations } from '@/lib/projects/application/operations' +import { type CreateProjectInput, createProjectInputSchema } from '@/lib/projects/create-input' +import { + createWorkspaceWithProjectInTransaction, + emitWorkspaceCreatedPlatformEvent, +} from '@/lib/workspaces/create' +import { + getWorkspaceCreationPolicy, + WorkspaceCreationCapabilityWithheldError, + WorkspaceCreationContextChangedError, +} from '@/lib/workspaces/policy' + +interface CreatedProject { + project: { id: string; name: string } + initialEnvironment: { id: string; name: string } +} + +export const createProject: OperationUseCase< + typeof projectOperations.create, + CreateProjectInput, + CreatedProject +> = { + operation: projectOperations.create, + async execute({ principal, input, request }) { + requireProjectPrincipal(principal, projectOperations.create) + const parsed = createProjectInputSchema.safeParse(input) + if (!parsed.success) + throw new OrchestrationError( + 'validation', + 'A scope, Project name and initial environment name are required' + ) + const { organizationId, name, initialEnvironment } = parsed.data + const policy = await getWorkspaceCreationPolicy({ + userId: principal.userId, + activeOrganizationId: organizationId, + pinOrganization: true, + }) + if (!policy.canCreate) { + if (policy.blockedReasonCode === 'permission-group-denied') + refuseCapability('workspace.create') + throw new OrchestrationError('forbidden', policy.reason ?? 'Project creation is not allowed') + } + if (policy.organizationId !== organizationId) { + throw new OrchestrationError( + 'forbidden', + 'The requested organization cannot create environments under its current subscription' + ) + } + let created: Awaited> + try { + created = await db.transaction((tx) => + createWorkspaceWithProjectInTransaction(tx, { + userId: principal.userId, + name: initialEnvironment.name, + projectName: name, + organizationId, + workspaceMode: policy.workspaceMode, + billedAccountUserId: policy.billedAccountUserId, + observedOrganizationId: policy.observedOrganizationId, + governingPermissionGroupOrganizationId: policy.governingPermissionGroupOrganizationId, + }) + ) + } catch (error) { + if (error instanceof WorkspaceCreationCapabilityWithheldError) + refuseCapability('workspace.create') + if (error instanceof WorkspaceCreationContextChangedError) + throw new OrchestrationError( + 'conflict', + 'Organization membership changed; retry Project creation' + ) + if (getPostgresErrorCode(error) === '55P03') + throw new OrchestrationError( + 'locked', + 'This organization is being updated; retry Project creation' + ) + if ( + getPostgresErrorCode(error) === '23503' && + getPostgresConstraintName(error) === 'workspace_owner_id_user_id_fk' + ) + throw new OrchestrationError('unauthorized', 'Unauthorized') + throw error + } + const environment = created.workspace + emitWorkspaceCreatedPlatformEvent({ + workspaceId: environment.id, + userId: principal.userId, + name: environment.name, + }) + recordProjectedUseCaseAuditEntries( + projectOperations.create, + environment.id, + principal, + request, + [ + { + action: AuditAction.PROJECT_CREATED, + resourceType: AuditResourceType.PROJECT, + resourceId: created.projectId, + resourceName: name, + description: `Created Project "${name}"`, + }, + { + action: AuditAction.WORKSPACE_CREATED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: environment.id, + resourceName: environment.name, + description: `Created workspace "${environment.name}"`, + metadata: { name: environment.name, workspaceMode: environment.workspaceMode }, + }, + ], + organizationId ?? undefined + ) + return { + project: { id: created.projectId, name }, + initialEnvironment: { id: environment.id, name: environment.name }, + } + }, +} diff --git a/apps/sim/lib/projects/application/index.ts b/apps/sim/lib/projects/application/index.ts index 22b91983164..2f3d714ebde 100644 --- a/apps/sim/lib/projects/application/index.ts +++ b/apps/sim/lib/projects/application/index.ts @@ -1,3 +1,4 @@ +export { createProject } from '@/lib/projects/application/create-project' export { projectOperations } from '@/lib/projects/application/operations' export { archiveProject, diff --git a/apps/sim/lib/projects/application/operations.ts b/apps/sim/lib/projects/application/operations.ts index d95eaa5c6e0..4158cd61008 100644 --- a/apps/sim/lib/projects/application/operations.ts +++ b/apps/sim/lib/projects/application/operations.ts @@ -1,5 +1,5 @@ import type { ApplicationOperation } from '@/lib/core/application/operation' -import { assertOperationCapability } from '@/lib/core/application/operation' +import { assertOperationCapability, defineOperation } from '@/lib/core/application/operation' export interface ProjectOperation extends ApplicationOperation { readonly principalKinds: readonly ['session'] @@ -14,6 +14,11 @@ function defineProjectOperation(operation: O): } export const projectOperations = { + create: defineOperation({ + id: 'projects.create', + principalKinds: ['session'], + capability: 'workspace.create', + }), // permission-group-exempt: navigation exposes only Projects containing accessible environments. list: defineProjectOperation({ id: 'projects.list', diff --git a/apps/sim/lib/projects/create-input.ts b/apps/sim/lib/projects/create-input.ts new file mode 100644 index 00000000000..4eb91f72df2 --- /dev/null +++ b/apps/sim/lib/projects/create-input.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const createProjectInputSchema = z.object({ + organizationId: z.string().trim().min(1).nullable(), + name: z.string().trim().min(1).max(100), + initialEnvironment: z.object({ name: z.string().trim().min(1).max(100) }), +}) +export type CreateProjectInput = z.output diff --git a/apps/sim/lib/projects/membership.ts b/apps/sim/lib/projects/membership.ts index 7a328d71702..cfd08e1a8c8 100644 --- a/apps/sim/lib/projects/membership.ts +++ b/apps/sim/lib/projects/membership.ts @@ -32,12 +32,13 @@ export async function createProjectForWorkspace( organizationId: string | null ownerId: string archivedAt?: Date | null + projectName?: string } ): Promise { const id = generateId() await tx.insert(project).values({ id, - name: generatedProjectName(input.name), + name: input.projectName ?? generatedProjectName(input.name), organizationId: input.organizationId, ownerId: input.ownerId, archivedAt: input.archivedAt ?? null, diff --git a/apps/sim/lib/workspaces/create.ts b/apps/sim/lib/workspaces/create.ts index b8aea562d5a..fd6210426bd 100644 --- a/apps/sim/lib/workspaces/create.ts +++ b/apps/sim/lib/workspaces/create.ts @@ -89,9 +89,10 @@ export interface TransactionalCreateWorkspaceParams extends CreateWorkspaceParam * permission-group advisory lock — before inserting the workspace, owner * permission and optional starter workflow atomically. */ -export async function createWorkspaceInTransaction( +export async function createWorkspaceWithProjectInTransaction( tx: DbTransaction, { + projectName, userId, observedOrganizationId, name, @@ -100,8 +101,8 @@ export async function createWorkspaceInTransaction( workspaceMode, billedAccountUserId, governingPermissionGroupOrganizationId, - }: TransactionalCreateWorkspaceParams -): Promise { + }: TransactionalCreateWorkspaceParams & { projectName?: string } +): Promise<{ projectId: string; workspace: CreatedWorkspace }> { const workspaceId = generateId() const workflowId = generateId() const now = new Date() @@ -130,7 +131,8 @@ export async function createWorkspaceInTransaction( updatedAt: now, }) - await createProjectForWorkspace(tx, { + const projectId = await createProjectForWorkspace(tx, { + projectName, workspaceId, name, organizationId: organizationId ?? null, @@ -186,18 +188,29 @@ export async function createWorkspaceInTransaction( } return { - id: workspaceId, - name, - ownerId: userId, - organizationId, - workspaceMode, - billedAccountUserId: committedBilledAccountUserId, - allowPersonalApiKeys: true, - createdAt: now, - updatedAt: now, + projectId, + workspace: { + id: workspaceId, + name, + ownerId: userId, + organizationId, + workspaceMode, + billedAccountUserId: committedBilledAccountUserId, + allowPersonalApiKeys: true, + createdAt: now, + updatedAt: now, + }, } } +/** Preserves the workspace-only result for existing creation callers. */ +export async function createWorkspaceInTransaction( + tx: DbTransaction, + params: TransactionalCreateWorkspaceParams +): Promise { + return (await createWorkspaceWithProjectInTransaction(tx, params)).workspace +} + /** Creates a workspace through the canonical lock-and-insert transaction. */ export async function createWorkspace(params: CreateWorkspaceParams) { /** diff --git a/docs/plans/project-entity-foundation.md b/docs/plans/project-entity-foundation.md index 4126b389c63..6bb232a0600 100644 --- a/docs/plans/project-entity-foundation.md +++ b/docs/plans/project-entity-foundation.md @@ -71,7 +71,7 @@ Do not add `project_member` for navigation or Issues. Extend the existing permis 2. Environment and Project organization scopes agree. Personal environments within a Project may have different lifecycle owners only if one explicitly selected Project owner governs Project lifecycle; that must not transfer environment ownership or grant access. 3. Fork creation inherits its parent's Project in the same transaction after rechecking current source scope under lock. Existing fork families seed Projects initially. Explicit disconnection splits the subtree into a new Project. Multiple roots can still arise through parent deletion; do not infer Issue movement from root counts alone. 4. Nested forks inherit the same Project; siblings need not have a unique depth. A disconnected fork and every descendant move together to a fresh Project. Never manufacture a new Project from a missing-parent/backfill race. Reevaluate Issue eligibility on both Projects after the split: a previously partial-access user may become full-access. Project-targeted permission-group restrictions also need explicit migration semantics; proposed preserve applicable restrictions on the new Project rather than accidentally resetting configured policy, while acknowledging the all-or-nothing rule now applies to each new environment set. Record policy handling and both Project IDs in the operation audit. Existing Issues stay with the original Project. Explicit user-requested migration through Sim Chat/Mothership must authorize source and destination Projects under one semantic application operation, with atomic/retry-safe movement and audit; it is an Issues feature, not a Project-foundation dependency. -5. Renaming an environment does not rename the Project; rename the Project through its own operation. Backfill and new Project creation use ` - Project`, as specified by the naming contract; workspace names remain untouched. Bound the generated Project name without losing its suffix, with `Untitled` for an empty source; surface any length normalization in the report. Duplicates are allowed and recorded, not merged. +5. Renaming an environment does not rename the Project; rename the Project through its own operation. Backfill and implicit Project creation through legacy workspace creation use ` - Project`; explicit Project creation accepts independent Project and initial environment names. Workspace names remain untouched. Bound the generated Project name without losing its suffix, with `Untitled` for an empty source; surface any length normalization in the report. Duplicates are allowed and recorded, not merged. 6. Individual environment delete/archive must leave at least one active environment in an active Project and at least one member environment overall. Block removal of the last environment; direct hard-delete, account deletion, moves and fork disconnect cannot bypass the invariant. Serialize on the Project so two concurrent removals cannot each see the other as the remaining environment. For disconnect/move, validate both resulting Projects; refuse a split that strands the original as active with no active environment. Coordinated whole-Project lifecycle operations are the explicit exception for active-count checks, not an adapter flag that callers can freely set. 7. Project archive is one authorized compound operation that archives every environment and workflow, including operational deactivation already owned by existing archive paths. Retain membership rows and Issues. Reject new environment/fork/workflow admission into an archived Project. Preserve pre-existing archived states. Current `lib/workspaces/lifecycle.ts` commits workspace/resource archival before calling `archiveWorkflowsForWorkspace`; looping this helper across environments would permit a partially archived Project. Refactor transaction-owning primitives so durable Project/environment/workflow state changes commit atomically, with existing required external effects queued/retried after commit. Failure must not be reported as complete while workflows remain active. Project restore is deferred until a complete inverse exists; do not ship a metadata-only unarchive that leaves no active environments or silently revives resources archived before the Project. Archival does not imply cancellation of already-running executions beyond existing archive semantics unless separately specified. 8. Moving one environment between organizations cannot retain its old Project membership. For initial shipment, refuse a partial Project transfer unless the operation explicitly names a destination/new Project and defines the Issue outcome; preserve original Project Issues. A whole-Project transfer requires authorization and disclosure for all environments, retained Project data, collaborators and Issues. @@ -272,13 +272,14 @@ The schema contains `project` (`id`, `name`, required `ownerId`, nullable `organ Implemented session routes: +- `POST /api/projects`: creates a named Project and its named first environment atomically, including admin permissions and a starter workflow. Requires explicit `organizationId` (or `null` for personal scope), `name`, and `initialEnvironment.name`; returns both IDs and names with HTTP 201. Uses existing workspace creation eligibility, billing and permission-group rules. - `GET /api/projects`: authorized, cursor-paginated active Project inventory. - `GET /api/projects/[id]`: metadata, accessible active environments, and administration/Issues capabilities. - `GET /api/projects/by-workspace/[workspaceId]`: canonical lookup that also authorizes the requested environment. - `PATCH /api/projects/[id]`: rename. - `DELETE /api/projects/[id]`: compound Project/environment/workflow archive. -Workspace creation creates its Project atomically; fork creation joins the existing Project. These are the creation surfaces in this foundation. A separate empty-Project create, arbitrary environment attachment, restore UI, public-key API, and Issues CRUD are not implemented here. The existing permission-group editor gains the Project Issues restriction; the new Project navigation UI remains a separate consumer. +Workspace creation creates its Project atomically; fork creation joins the existing Project. Existing workspace creation callers retain their response shape and generated Project names. Project creation shares their transaction primitive; it cannot attach an arbitrary environment or create an empty Project. A separate empty-Project create, arbitrary environment attachment, restore UI, public-key API, and Issues CRUD are not implemented here. The existing permission-group editor gains the Project Issues restriction; the new Project navigation UI remains a separate consumer. `deniedPartialAccessProjectIssues` is a Project-ID denylist on existing permission-group config. An empty list allows access. For a partial-access teammate, each accessible active environment resolves its effective group using existing explicit-member/all-members/default precedence. If any of those effective groups denies this Project, all Issues are denied. The selected environment cannot change the answer. Archived environments are excluded from this comparison. Full-access teammates bypass this partial-access restriction. Issue use cases call `authorizeProject` in their own transaction before reading or writing Issue rows; `getProjectIssueAccess` is only a read-only probe. The Issue gate holds the permission-group lock through that transaction, so callers must respect its existing leaf-lock rule. diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 6505bf2431a..06547294cf5 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -218,6 +218,7 @@ export const AuditAction = { WORKFLOW_EXPORTED: 'workflow.exported', WORKSPACE_CREATED: 'workspace.created', + PROJECT_CREATED: 'project.created', PROJECT_UPDATED: 'project.updated', PROJECT_ARCHIVED: 'project.archived', WORKSPACE_UPDATED: 'workspace.updated', From 91cba23a84dd4ca5211f3a90c168620599a2a3c7 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 02:10:54 -0700 Subject: [PATCH 3/8] docs(projects): record project files follow-up --- docs/plans/project-entity-foundation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/plans/project-entity-foundation.md b/docs/plans/project-entity-foundation.md index 6bb232a0600..c798f58d926 100644 --- a/docs/plans/project-entity-foundation.md +++ b/docs/plans/project-entity-foundation.md @@ -310,3 +310,9 @@ bun --no-env-file packages/db/scripts/backfill-projects.ts apply \ Validation artifacts come from `PROJECT_FOUNDATION_REPORT_PATH` (or `apps/sim/test-results/project-foundation.json`) and the integration runner’s JSON report. CI already uploads `apps/sim/test-results/*.json`. The former mock-count workspace lifecycle tests were replaced by real database checks of concurrent removal and retry repair. The dedicated backfill suite executes the exact additive migration in a disposable database before exercising backfill. External webhook/socket/MCP cleanup retains existing best-effort post-commit behavior; durable archive state commits atomically. The operator-run CLI procedure, self-hosted commands, SQLSTATE handling, lock behavior, report interpretation and repair procedure are documented in [the Project backfill runbook](./project-backfill-runbook.md). + +### Follow-up: Project-scoped files + +Project names provide the identifying label; this foundation intentionally omits a separate description field. Shared purpose, goals, and operating guidance belong in Project-owned files, with a designated brief identified by a stable document ID. + +Extend file ownership to Projects in a separate PR, likely with nullable `projectId` on the existing files table and constraints for valid scope combinations. Include authorization, version history/collaboration, storage lifecycle, and Mothership tools/VFS support. Keep environment files restricted to their environments; publishing them to a Project must explicitly account for broader readership. Project-scoped files and brief consumption are not implemented in this foundation. From 0980ad1ffbab165eaa5f877b6aae485567898ed4 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 11:04:36 -0700 Subject: [PATCH 4/8] docs(projects): explain project and workspace creation flows --- apps/sim/lib/projects/README.md | 76 +++++++++++++++++++++++++ docs/plans/project-entity-foundation.md | 2 + 2 files changed, 78 insertions(+) create mode 100644 apps/sim/lib/projects/README.md diff --git a/apps/sim/lib/projects/README.md b/apps/sim/lib/projects/README.md new file mode 100644 index 00000000000..3b95d0a8195 --- /dev/null +++ b/apps/sim/lib/projects/README.md @@ -0,0 +1,76 @@ +# Project creation + +A Project groups environments. An environment is an existing `workspace` record; there is no separate environment table. Every newly created Project starts with an environment. + +## Choose the creation flow + +| Caller | Flow | Result | +| --- | --- | --- | +| New Project onboarding | `POST /api/projects` | Creates a Project and its first environment together, with independently supplied names. | +| Existing workspace creation UI or caller | `POST /api/workspaces` | Creates a workspace and automatically creates its Project, preserving the existing workspace response. | +| Create another environment by forking | Existing workspace fork operation | Creates a child workspace in the source workspace's Project. | + +Both POST endpoints are internal, session-authenticated APIs. `POST /api/projects` is not a public `/api/v2` endpoint and does not accept API-key principals. This foundation does not remove or deprecate existing workspace creation endpoints. + +Do not call both creation endpoints for one onboarding flow: each creates a new workspace and a new Project. Neither endpoint attaches a workspace to an existing Project. A general Project environment-creation endpoint is follow-up work. + +## Create a Project and its first environment + +Use the shared client and contract for same-origin application calls: + +```ts +import { requestJson } from '@/lib/api/client/request' +import { createProjectContract } from '@/lib/api/contracts/projects' + +const result = await requestJson(createProjectContract, { + body: { + organizationId: selectedOrganizationId, + name: 'Customer support', + initialEnvironment: { name: 'Production' }, + }, +}) +``` + +All three inputs are required: + +- `organizationId`: the intended organization's ID, or explicitly `null` for a personal Project. There is no fallback to the session's active organization. The caller must be eligible to create in the requested scope; an ineligible organization request is not silently converted to personal creation. +- `name`: the Project name, trimmed and limited to 1–100 characters. +- `initialEnvironment.name`: the first environment's name, also trimmed and limited to 1–100 characters. There is **no default environment name**; `Production` above is an example supplied by the caller. + +For personal creation, use the same request with `organizationId: null`. + +The HTTP 201 response contains both IDs and names: + +```json +{ + "project": { "id": "", "name": "Customer support" }, + "initialEnvironment": { "id": "", "name": "Production" } +} +``` + +`initialEnvironment.id` is the workspace ID for existing workspace routes and navigation. + +The application operation creates the Project, workspace, Project membership, initial administrator permissions, and starter workflow in one database transaction. A failed transaction leaves none of these new records committed. This endpoint always includes the starter workflow; it has no `skipDefaultWorkflow` option. + +Creation uses existing workspace eligibility, billing, and `workspace.create` permission-group rules. Choosing personal scope does not bypass applicable organization permission-group restrictions. This endpoint has no idempotency-key support: resubmitting after an uncertain network result can create another Project, so do not blindly retry. + +## Existing workspace creation + +Existing callers can continue to use `createWorkspaceContract` and `POST /api/workspaces` with their current body: + +```json +{ + "name": "Support workspace", + "skipDefaultWorkflow": false +} +``` + +`skipDefaultWorkflow` remains optional and defaults to `false`. The route uses the session's active organization and existing workspace creation policy to resolve ownership and billing. It does not take the explicit Project scope or independent Project name used by `POST /api/projects`. + +The workspace and its Project are still created atomically. The generated Project name is `Support workspace - Project`; long names are bounded to 100 characters while retaining the suffix. The response remains `{ "workspace": ... }` with HTTP 200, without a new Project response wrapper. Call `GET /api/projects/by-workspace/[workspaceId]` when an existing workspace caller needs its authorized Project details. + +## Server implementation + +The Project route calls `createProject` in `application/create-project.ts`. Existing workspace callers continue through their current creation paths. Both use the shared transaction primitive in `lib/workspaces/create.ts`; surface adapters must not independently commit Project and workspace creation. + +Project descriptions and Project-scoped files are not part of this creation contract. Project-scoped files and a designated Project brief are follow-up work documented in the foundation plan. diff --git a/docs/plans/project-entity-foundation.md b/docs/plans/project-entity-foundation.md index c798f58d926..f2442f8fb8b 100644 --- a/docs/plans/project-entity-foundation.md +++ b/docs/plans/project-entity-foundation.md @@ -270,6 +270,8 @@ Every Project retains a required `ownerId`, including organization Projects. `or The schema contains `project` (`id`, `name`, required `ownerId`, nullable `organizationId`, `archivedAt`, `createdAt`, `updatedAt`) and `project_workspace` (`projectId`, unique `workspaceId`, `createdAt`). There is no stored fork depth, position, or Project billing state. +For request/response examples and the distinction between explicit Project creation and existing workspace creation, see the [Project creation guide](../../apps/sim/lib/projects/README.md). + Implemented session routes: - `POST /api/projects`: creates a named Project and its named first environment atomically, including admin permissions and a starter workflow. Requires explicit `organizationId` (or `null` for personal scope), `name`, and `initialEnvironment.name`; returns both IDs and names with HTTP 201. Uses existing workspace creation eligibility, billing and permission-group rules. From a14b4489ba40aacf1904ac20be0e3380bf8c3fd2 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 13:20:49 -0700 Subject: [PATCH 5/8] fix(projects): stage activation after compatible writers deploy --- .../ee/workspace-forking/lib/create-fork.ts | 7 +- .../lib/billing/organizations/membership.ts | 5 +- apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/projects/README.md | 10 +- .../__integration__/foundation.integration.ts | 301 +++++++++++++++- apps/sim/lib/projects/account-deletion.ts | 3 +- .../projects/application/create-project.ts | 2 + .../sim/lib/projects/application/use-cases.ts | 7 + apps/sim/lib/projects/membership.ts | 42 ++- apps/sim/lib/projects/rollout.server.ts | 22 ++ apps/sim/lib/workspaces/create.ts | 42 ++- docs/plans/project-backfill-runbook.md | 68 ---- docs/plans/project-entity-foundation.md | 320 ------------------ packages/db/scripts/backfill-projects.ts | 49 ++- .../scripts/project-backfill.integration.ts | 31 ++ 15 files changed, 489 insertions(+), 422 deletions(-) create mode 100644 apps/sim/lib/projects/rollout.server.ts delete mode 100644 docs/plans/project-backfill-runbook.md delete mode 100644 docs/plans/project-entity-foundation.md diff --git a/apps/sim/ee/workspace-forking/lib/create-fork.ts b/apps/sim/ee/workspace-forking/lib/create-fork.ts index 1192b8985f2..bc73206df66 100644 --- a/apps/sim/ee/workspace-forking/lib/create-fork.ts +++ b/apps/sim/ee/workspace-forking/lib/create-fork.ts @@ -314,9 +314,10 @@ export async function createFork(params: CreateForkParams): Promise { + vi.stubEnv('PROJECT_WRITES_ENABLED', 'true') + vi.stubEnv('PROJECT_API_ENABLED', 'true') +}) + const users: string[] = [] const organizations: string[] = [] const environments: string[] = [] @@ -182,6 +193,292 @@ afterAll(async () => { }) describe('Project foundation at the database and application boundary', () => { + check( + 'disabled rollout preserves legacy creation and fork/disconnect without Project rows', + async () => { + vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') + vi.stubEnv('PROJECT_API_ENABLED', 'false') + const f = await fixture(false, 1) + const source = await db.transaction((tx) => + createWorkspaceInTransaction(tx, { + userId: f.ownerId, + name: 'Legacy source', + organizationId: null, + observedOrganizationId: null, + governingPermissionGroupOrganizationId: null, + workspaceMode: 'personal', + billedAccountUserId: f.ownerId, + skipDefaultWorkflow: true, + }) + ) + environments.push(source.id) + expect( + await db.select().from(projectWorkspace).where(eq(projectWorkspace.workspaceId, source.id)) + ).toEqual([]) + const parent = await getWorkspaceWithOwner(source.id) + if (!parent) throw new Error('Missing source fixture') + const fork = await createFork({ + source: parent, + policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), + userId: f.ownerId, + name: 'Legacy child', + }) + environments.push(fork.workspace.id) + expect( + await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, fork.workspace.id)) + ).toEqual([]) + await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) + const [child] = await db.select().from(workspace).where(eq(workspace.id, fork.workspace.id)) + expect(child.forkedFromWorkspaceId).toBeNull() + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(1) + } + ) + + check( + 'Project operations remain unavailable until API activation with no partial creation', + async () => { + const f = await fixture(false, 1) + vi.stubEnv('PROJECT_API_ENABLED', 'false') + for (const writes of ['false', 'true']) { + vi.stubEnv('PROJECT_WRITES_ENABLED', writes) + const input = { projectId: f.projectId } + const calls = [ + () => + createProject.execute({ + principal: f.owner, + input: { + organizationId: null, + name: 'Blocked', + initialEnvironment: { name: 'Production' }, + }, + request, + }), + () => getProject.execute({ principal: f.owner, input, request }), + () => + getWorkspaceProject.execute({ + principal: f.owner, + input: { workspaceId: f.ids[0] }, + request, + }), + () => listProjects.execute({ principal: f.owner, input: { limit: 10 }, request }), + () => + renameProject.execute({ + principal: f.owner, + input: { ...input, name: 'Blocked' }, + request, + }), + () => archiveProject.execute({ principal: f.owner, input, request }), + () => getProjectIssueAccess.execute({ principal: f.owner, input, request }), + ] + for (const call of calls) await expect(call()).rejects.toMatchObject({ statusCode: 503 }) + expect( + await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) + ).toHaveLength(1) + const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) + expect(record.name).toBe('Environment 0 - Project') + expect(record.archivedAt).toBeNull() + } + } + ) + + check( + 'disabling activation preserves assigned fork membership and lifecycle protections', + async () => { + const f = await fixture(false, 1) + vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') + vi.stubEnv('PROJECT_API_ENABLED', 'false') + const parent = await getWorkspaceWithOwner(f.ids[0]) + if (!parent) throw new Error('Missing source fixture') + const fork = await createFork({ + source: parent, + policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), + userId: f.ownerId, + name: 'Assigned child', + }) + environments.push(fork.workspace.id) + const [membership] = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, fork.workspace.id)) + expect(membership.projectId).toBe(f.projectId) + await unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: fork.workspace.id }) + const [detached] = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, fork.workspace.id)) + expect(detached.projectId).not.toBe(f.projectId) + await expect( + archiveWorkspace(fork.workspace.id, { requestId: 'disabled-project-rollout' }) + ).rejects.toMatchObject({ code: 'conflict' }) + } + ) + + check('writer activation assigns new workspaces while Project APIs remain disabled', async () => { + vi.stubEnv('PROJECT_API_ENABLED', 'false') + const f = await fixture(false, 1) + const created = await db.transaction((tx) => + createWorkspaceInTransaction(tx, { + userId: f.ownerId, + name: 'Writer activation', + organizationId: null, + observedOrganizationId: null, + governingPermissionGroupOrganizationId: null, + workspaceMode: 'personal', + billedAccountUserId: f.ownerId, + skipDefaultWorkflow: true, + }) + ) + environments.push(created.id) + expect( + await db.select().from(projectWorkspace).where(eq(projectWorkspace.workspaceId, created.id)) + ).toHaveLength(1) + vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') + vi.stubEnv('PROJECT_API_ENABLED', 'true') + await expect( + createProject.execute({ + principal: f.owner, + input: { organizationId: null, name: 'Invalid', initialEnvironment: { name: 'First' } }, + request, + }) + ).rejects.toThrow('PROJECT_API_ENABLED requires PROJECT_WRITES_ENABLED') + expect(await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId))).toHaveLength( + 2 + ) + }) + + check('legacy fork and disconnect refuse a partially assigned subtree', async () => { + const f = await fixture(false, 3) + await db + .delete(projectWorkspace) + .where(inArray(projectWorkspace.workspaceId, f.ids.slice(0, 2))) + vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') + vi.stubEnv('PROJECT_API_ENABLED', 'false') + const parent = await getWorkspaceWithOwner(f.ids[1]) + if (!parent) throw new Error('Missing source fixture') + await expect( + createFork({ + source: parent, + policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), + userId: f.ownerId, + name: 'Invalid child', + }) + ).rejects.toMatchObject({ code: 'conflict' }) + await expect( + unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] }) + ).rejects.toMatchObject({ code: 'conflict' }) + const [child] = await db.select().from(workspace).where(eq(workspace.id, f.ids[1])) + expect(child.forkedFromWorkspaceId).toBe(f.ids[0]) + expect(await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId))).toHaveLength( + 3 + ) + }) + + check( + 'legacy membership decisions exclude backfill until the writing transaction commits', + async () => { + const f = await fixture(false, 1) + await db.delete(projectWorkspace).where(eq(projectWorkspace.workspaceId, f.ids[0])) + await db.delete(project).where(eq(project.id, f.projectId)) + const read = createDeferred() + const release = createDeferred() + const writer = db.transaction(async (tx) => { + expect(await lockWorkspaceProject(tx, f.ids[0])).toBeNull() + read.resolve() + await release.promise + await tx + .update(workspace) + .set({ name: 'Concurrent legacy edit' }) + .where(eq(workspace.id, f.ids[0])) + }) + try { + await Promise.race([read.promise, writer]) + await expect( + db.transaction(async (tx) => { + await tx.execute( + sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + ) + }) + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '55P03') + } finally { + release.resolve() + await writer + } + await db.transaction(async (tx) => { + await tx.execute( + sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + ) + await createProjectForWorkspace(tx, { + workspaceId: f.ids[0], + name: 'Concurrent legacy edit', + organizationId: null, + ownerId: f.ownerId, + }) + }) + expect( + await db.select().from(projectWorkspace).where(eq(projectWorkspace.workspaceId, f.ids[0])) + ).toHaveLength(1) + } + ) + + check('a fork waiting for backfill inherits membership committed before it resumes', async () => { + const f = await fixture(false, 1) + await db.delete(projectWorkspace).where(eq(projectWorkspace.workspaceId, f.ids[0])) + await db.delete(project).where(eq(project.id, f.projectId)) + const parent = await getWorkspaceWithOwner(f.ids[0]) + if (!parent) throw new Error('Missing source fixture') + const policy = await getWorkspaceCreationPolicy({ userId: f.ownerId }) + const locked = createDeferred() + const release = createDeferred() + const backfill = db.transaction(async (tx) => { + await tx.execute( + sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + ) + locked.resolve() + await release.promise + return createProjectForWorkspace(tx, { + workspaceId: f.ids[0], + name: 'Backfilled source', + organizationId: null, + ownerId: f.ownerId, + }) + }) + await Promise.race([locked.promise, backfill]) + const fork = createFork({ + source: parent, + policy, + userId: f.ownerId, + name: 'Concurrent child', + }).then((result) => { + environments.push(result.workspace.id) + return result + }) + let blocked = false + try { + for (let attempt = 0; attempt < 100; attempt++) { + const rows = await db.execute<{ waiting: boolean }>(sql`SELECT EXISTS ( + SELECT 1 FROM pg_locks WHERE relation = 'workspace'::regclass AND mode = 'RowExclusiveLock' AND NOT granted + ) AS waiting`) + if (rows[0]?.waiting) { + blocked = true + break + } + await sleep(20) + } + } finally { + release.resolve() + } + const [projectId, result] = await Promise.all([backfill, fork]) + expect(blocked).toBe(true) + const [membership] = await db + .select() + .from(projectWorkspace) + .where(eq(projectWorkspace.workspaceId, result.workspace.id)) + expect(membership.projectId).toBe(projectId) + }) + check( 'Project creation commits its named first environment and starter workflow in the requested scope', async () => { diff --git a/apps/sim/lib/projects/account-deletion.ts b/apps/sim/lib/projects/account-deletion.ts index 541ea922767..795c999dd57 100644 --- a/apps/sim/lib/projects/account-deletion.ts +++ b/apps/sim/lib/projects/account-deletion.ts @@ -2,7 +2,7 @@ import { permissions, project, projectWorkspace, workspace } from '@sim/db/schem import { and, asc, eq, inArray, ne, or, sql } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbTransaction } from '@/lib/db/types' -import { lockProject } from '@/lib/projects/membership' +import { lockProject, lockProjectBackfillWrites } from '@/lib/projects/membership' /** Account teardown may erase a wholly private Project, but never strand a surviving one. */ export async function prepareProjectsForAccountDeletion( @@ -10,6 +10,7 @@ export async function prepareProjectsForAccountDeletion( userId: string, doomedWorkspaceIds: string[] ): Promise { + await lockProjectBackfillWrites(tx) const records = await tx .select({ id: project.id }) .from(project) diff --git a/apps/sim/lib/projects/application/create-project.ts b/apps/sim/lib/projects/application/create-project.ts index dec1a3ea376..b3d039b8e64 100644 --- a/apps/sim/lib/projects/application/create-project.ts +++ b/apps/sim/lib/projects/application/create-project.ts @@ -8,6 +8,7 @@ import { refuseCapability } from '@/lib/permission-groups/capabilities' import { requireProjectPrincipal } from '@/lib/projects/application/authorization' import { projectOperations } from '@/lib/projects/application/operations' import { type CreateProjectInput, createProjectInputSchema } from '@/lib/projects/create-input' +import { requireProjectApiEnabled } from '@/lib/projects/rollout.server' import { createWorkspaceWithProjectInTransaction, emitWorkspaceCreatedPlatformEvent, @@ -31,6 +32,7 @@ export const createProject: OperationUseCase< operation: projectOperations.create, async execute({ principal, input, request }) { requireProjectPrincipal(principal, projectOperations.create) + requireProjectApiEnabled() const parsed = createProjectInputSchema.safeParse(input) if (!parsed.success) throw new OrchestrationError( diff --git a/apps/sim/lib/projects/application/use-cases.ts b/apps/sim/lib/projects/application/use-cases.ts index d67a2c52b89..eb7484471d8 100644 --- a/apps/sim/lib/projects/application/use-cases.ts +++ b/apps/sim/lib/projects/application/use-cases.ts @@ -8,6 +8,7 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { authorizeProject, requireProjectPrincipal } from '@/lib/projects/application/authorization' import { projectOperations } from '@/lib/projects/application/operations' import { archiveProjectInTransaction, finishProjectArchive } from '@/lib/projects/lifecycle' +import { requireProjectApiEnabled } from '@/lib/projects/rollout.server' interface ProjectInput { projectId: string @@ -31,6 +32,7 @@ export const getProject: OperationUseCase< operation: projectOperations.get, async execute({ principal, input }) { requireProjectPrincipal(principal, projectOperations.get) + requireProjectApiEnabled() return db.transaction(async (tx) => ({ project: presentProject(await authorizeProject(tx, principal, projectOperations.get, input)), })) @@ -46,6 +48,7 @@ export const getProjectIssueAccess: OperationUseCase< operation: projectOperations.issues, async execute({ principal, input }) { requireProjectPrincipal(principal, projectOperations.issues) + requireProjectApiEnabled() return db.transaction(async (tx) => { const context = await authorizeProject(tx, principal, projectOperations.issues, input) return { projectId: context.record.id } @@ -61,6 +64,7 @@ export const listProjects: OperationUseCase< operation: projectOperations.list, async execute({ principal, input }) { requireProjectPrincipal(principal, projectOperations.list) + requireProjectApiEnabled() if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100) throw new OrchestrationError('validation', 'Limit must be between 1 and 100') return db.transaction(async (tx) => { @@ -108,6 +112,7 @@ export const renameProject: OperationUseCase< operation: projectOperations.rename, async execute({ principal, input, request }) { requireProjectPrincipal(principal, projectOperations.rename) + requireProjectApiEnabled() const name = input.name.trim() if (!name || name.length > 100) throw new OrchestrationError('validation', 'Project name must contain 1–100 characters') @@ -149,6 +154,7 @@ export const archiveProject: OperationUseCase< operation: projectOperations.archive, async execute({ principal, input, request }) { requireProjectPrincipal(principal, projectOperations.archive) + requireProjectApiEnabled() const result = await db.transaction(async (tx) => { const context = await authorizeProject(tx, principal, projectOperations.archive, input) const effects = await archiveProjectInTransaction(tx, context.record.id) @@ -184,6 +190,7 @@ export const getWorkspaceProject: OperationUseCase< operation: projectOperations.get, async execute({ principal, input }) { requireProjectPrincipal(principal, projectOperations.get) + requireProjectApiEnabled() return db.transaction(async (tx) => { const [membership] = await tx .select({ projectId: projectWorkspace.projectId }) diff --git a/apps/sim/lib/projects/membership.ts b/apps/sim/lib/projects/membership.ts index cfd08e1a8c8..54c45c6e36c 100644 --- a/apps/sim/lib/projects/membership.ts +++ b/apps/sim/lib/projects/membership.ts @@ -5,6 +5,18 @@ import { and, asc, eq, inArray, isNull, ne, notInArray, sql } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +/** Prevents backfill from assigning membership between an absence read and the ensuing write. */ +export async function lockProjectBackfillWrites(tx: DbTransaction): Promise { + await tx.execute(sql`SET LOCAL lock_timeout = '5s'`) + try { + await tx.execute(sql`LOCK TABLE workspace IN ROW EXCLUSIVE MODE`) + } catch (error) { + if (getPostgresErrorCode(error) === '55P03') + throw new OrchestrationError('conflict', 'Project backfill is running; retry the operation') + throw error + } +} + /** Canonical Project mutex; membership and lifecycle writers hold it until commit. */ export async function lockProject(tx: DbTransaction, projectId: string): Promise { await tx.execute(sql`SELECT set_config('lock_timeout', '5000ms', true)`) @@ -49,6 +61,7 @@ export async function createProjectForWorkspace( /** Returns null only for a legacy workspace awaiting the operator-run backfill. */ export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: string) { + await lockProjectBackfillWrites(tx) const [membership] = await tx .select() .from(projectWorkspace) @@ -70,13 +83,28 @@ export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: strin export async function requireForkProject(tx: DbTransaction, parentWorkspaceId: string) { const parent = await lockWorkspaceProject(tx, parentWorkspaceId) - if (!parent) + if (!parent) { + await requireUnassignedForkSubtree(tx, parentWorkspaceId) + return null + } + if (parent.archivedAt) throw new OrchestrationError('conflict', 'Cannot fork an archived Project') + return parent +} + +/** Legacy fallback must not hide partially assigned descendants. Caller holds the lineage lock. */ +async function requireUnassignedForkSubtree(tx: DbTransaction, workspaceId: string): Promise { + const rows = await tx.execute<{ id: string }>(sql` + WITH RECURSIVE descendants AS ( + SELECT id FROM workspace WHERE id = ${workspaceId} + UNION + SELECT w.id FROM workspace w JOIN descendants d ON w.forked_from_workspace_id = d.id + ) SELECT d.id FROM descendants d JOIN project_workspace pw ON pw.workspace_id = d.id LIMIT 1 + `) + if (rows.length) throw new OrchestrationError( 'conflict', - 'This workspace needs the Project backfill before it can be forked' + 'Fork descendants need Project membership reconciliation' ) - if (parent.archivedAt) throw new OrchestrationError('conflict', 'Cannot fork an archived Project') - return parent } /** Individual removal cannot leave an active Project without an active environment. */ @@ -112,7 +140,10 @@ export async function splitForkProject( workspaceId: string ): Promise { const owner = await lockWorkspaceProject(tx, workspaceId) - if (!owner) return null + if (!owner) { + await requireUnassignedForkSubtree(tx, workspaceId) + return null + } if (owner.archivedAt) throw new OrchestrationError('conflict', 'Cannot disconnect an archived Project') const rows = await tx.execute<{ @@ -187,6 +218,7 @@ export async function transferWorkspaceProjects( ownerId?: string ): Promise { if (!workspaceIds.length) return + await lockProjectBackfillWrites(tx) const owners = await tx .selectDistinct({ id: projectWorkspace.projectId }) .from(projectWorkspace) diff --git a/apps/sim/lib/projects/rollout.server.ts b/apps/sim/lib/projects/rollout.server.ts new file mode 100644 index 00000000000..3ffe5d81e8c --- /dev/null +++ b/apps/sim/lib/projects/rollout.server.ts @@ -0,0 +1,22 @@ +import { envBoolean, getEnv } from '@/lib/core/config/env' +import { HttpError } from '@/lib/core/utils/http-error' + +/** Deployment-wide controls; existing Project lifecycle maintenance never depends on these. */ +export function getProjectRollout() { + const writesEnabled = envBoolean(getEnv('PROJECT_WRITES_ENABLED')) ?? false + const apiEnabled = envBoolean(getEnv('PROJECT_API_ENABLED')) ?? false + if (apiEnabled && !writesEnabled) + throw new Error('PROJECT_API_ENABLED requires PROJECT_WRITES_ENABLED') + return { writesEnabled, apiEnabled } +} + +class ProjectUnavailableError extends HttpError { + readonly statusCode = 503 + constructor() { + super('Projects are not enabled on this deployment') + } +} + +export function requireProjectApiEnabled(): void { + if (!getProjectRollout().apiEnabled) throw new ProjectUnavailableError() +} diff --git a/apps/sim/lib/workspaces/create.ts b/apps/sim/lib/workspaces/create.ts index fd6210426bd..d81847a487f 100644 --- a/apps/sim/lib/workspaces/create.ts +++ b/apps/sim/lib/workspaces/create.ts @@ -6,6 +6,7 @@ import { generateId } from '@sim/utils/id' import { PlatformEvents } from '@/lib/core/telemetry' import type { DbTransaction } from '@/lib/db/types' import { createProjectForWorkspace } from '@/lib/projects/membership' +import { getProjectRollout, requireProjectApiEnabled } from '@/lib/projects/rollout.server' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -89,10 +90,11 @@ export interface TransactionalCreateWorkspaceParams extends CreateWorkspaceParam * permission-group advisory lock — before inserting the workspace, owner * permission and optional starter workflow atomically. */ -export async function createWorkspaceWithProjectInTransaction( +async function createWorkspaceRecordsInTransaction( tx: DbTransaction, { projectName, + assignProject, userId, observedOrganizationId, name, @@ -101,8 +103,8 @@ export async function createWorkspaceWithProjectInTransaction( workspaceMode, billedAccountUserId, governingPermissionGroupOrganizationId, - }: TransactionalCreateWorkspaceParams & { projectName?: string } -): Promise<{ projectId: string; workspace: CreatedWorkspace }> { + }: TransactionalCreateWorkspaceParams & { projectName?: string; assignProject: boolean } +): Promise<{ projectId: string | null; workspace: CreatedWorkspace }> { const workspaceId = generateId() const workflowId = generateId() const now = new Date() @@ -131,13 +133,15 @@ export async function createWorkspaceWithProjectInTransaction( updatedAt: now, }) - const projectId = await createProjectForWorkspace(tx, { - projectName, - workspaceId, - name, - organizationId: organizationId ?? null, - ownerId: userId, - }) + const projectId = assignProject + ? await createProjectForWorkspace(tx, { + projectName, + workspaceId, + name, + organizationId: organizationId ?? null, + ownerId: userId, + }) + : null const permissionRows = [ { @@ -203,12 +207,28 @@ export async function createWorkspaceWithProjectInTransaction( } } +/** Explicit Project creation always commits its first environment in the same transaction. */ +export async function createWorkspaceWithProjectInTransaction( + tx: DbTransaction, + params: TransactionalCreateWorkspaceParams & { projectName?: string } +): Promise<{ projectId: string; workspace: CreatedWorkspace }> { + requireProjectApiEnabled() + const created = await createWorkspaceRecordsInTransaction(tx, { ...params, assignProject: true }) + if (!created.projectId) throw new Error('Project creation did not assign a Project') + return { ...created, projectId: created.projectId } +} + /** Preserves the workspace-only result for existing creation callers. */ export async function createWorkspaceInTransaction( tx: DbTransaction, params: TransactionalCreateWorkspaceParams ): Promise { - return (await createWorkspaceWithProjectInTransaction(tx, params)).workspace + return ( + await createWorkspaceRecordsInTransaction(tx, { + ...params, + assignProject: getProjectRollout().writesEnabled, + }) + ).workspace } /** Creates a workspace through the canonical lock-and-insert transaction. */ diff --git a/docs/plans/project-backfill-runbook.md b/docs/plans/project-backfill-runbook.md deleted file mode 100644 index d5154830401..00000000000 --- a/docs/plans/project-backfill-runbook.md +++ /dev/null @@ -1,68 +0,0 @@ -# Project backfill operations - -The SQL migration creates empty Project tables during the normal migration job. This backfill is a separate post-deployment operation. It is not registered in the startup migration registry: old replicas are still serving when that registry runs. - -## Operator execution - -Run the CLI from an approved maintenance host or a one-off migration container matching the deployed application revision. There is no dedicated GitHub Actions workflow and no automatic migration-registry entry for this backfill. The normal deployment pipeline still applies the additive SQL migration. - -Before apply: - -- Finish deploying the compatible release, including draining old workers. Pause manual lineage/ownership maintenance and coordinate a window without concurrent deployments. Without a workflow wrapper, the operator owns these checks; the CLI does not verify ECS cutover or serialize deployment pipelines. -- Supply `PROJECT_BACKFILL_DATABASE_URL` through the approved secret mechanism. Use a direct session-capable primary connection, with no pooled application DSN fallback. Verify the environment and the fingerprint printed by `identity`. -- Use a persistent, writable report directory. Retain the exact code revision, dry-run plan, apply reports, verification reports and console output with the release record. Reports contain resource metadata and should have restricted access. -- Rehearse staging using the same command before production. This task has not executed a hosted backfill or established an existing maintenance host. - -1. Run `dry-run` and inspect the JSON report: roots, environment IDs, planned Project IDs, proposed names, changes and conflicts. -2. Repair reported conflicts deliberately and produce a fresh dry run. Apply refuses incomplete or conflicted plans. -3. Run `apply --from-file --database-id --writers-drained`. Keep the same reviewed code revision and original plan for retries. The acknowledgement does not stop writers; each transaction applies one planned family after checking for changes. -4. Run `verify` independently. Require `completed: true`, `ready: true`, zero conflicts and zero missing assignments. Apply success alone does not establish readiness. -5. Repeat verification after normal writes resume. Keep Project/Issues consumers disabled until reconciliation and the separately deployed database membership/minimum-environment constraints are complete. This command never flips an activation flag. - -A canceled or failed apply may have committed earlier families. Retain its report and original dry-run artifact; reapply the same artifact after checking the failure. Existing matching planned IDs are reused. A changed code revision requires reviewing compatibility and normally a new dry-run plan. Never produce a new plan merely to hide an unexpected assignment. - -## Safety and failure handling - -- Dry-run and verify do not change database rows. All modes take a session advisory lock so two operator runs cannot overlap. The reserved connection and backend PID are checked before each batch; losing that connection stops the run. -- Discovery pages contain 100 workspace IDs. Each family defaults to at most 1,000 members at the CLI, with an explicit maximum of 10,000. Total discovery is capped at 250,000 workspaces; lineage ancestry is capped at 1,000 links. Oversized data needs a reviewed adjustment or separate migration, never an unbounded run. -- Apply takes `SHARE ROW EXCLUSIVE ... NOWAIT` locks on workspace, Project and membership tables before rediscovering the family. This excludes concurrent inserts/deletes/updates for the short transaction, including new descendants. Reads remain available. Busy writers cause immediate contention rather than an indefinitely queued lock. This is a maintenance operation: ordinary workspace writes can briefly wait while a batch holds its locks. -- A SHA-256 fingerprint covers the ordered environment records, including names, owners, organization, archive timestamps and fork connections. Any change since dry-run rejects that family. Existing Project assignments must match the planned Project ID. Existing names are retained. -- Reads retry transient connection/unavailability failures with bounded jitter. Writes retry only SQLSTATE `40001` (serialization), `40P01` (deadlock) and `55P03` (lock unavailable), at most three attempts. No connection-error write retry: the commit result may be unknown. Constraint errors, cancellation, statement timeouts and unknown errors stop the run. -- Lock timeout is 2 seconds; statements and transactions are capped at 30 seconds. PostgreSQL 17+ is required for the transaction timeout. Runtime is bounded between batches (default 10 minutes, maximum one hour), so the last transaction may finish after the budget. SIGINT/SIGTERM stop after the current transaction; forced termination rolls back an open transaction. -- Reports are replaced atomically before any write and after every committed family. Counts reflect acknowledged commits only. If the connection disappears at commit, counts can underreport; reconcile through the original plan and verification. A failed report write stops further batches; the last durable report remains available. Do not infer rollback from a missing final report. -- Reports distinguish planned counts in dry-run/verify from committed counts in apply. `completed` means the selected scan/plan was processed; `ready` is set only by successful verification. SQLSTATE is recorded without dumping database error payloads or credentials. - -## Conflict repair - -Report entries include the root/environment IDs, relevant Project IDs and reason. Examples: - -- Multiple Projects in one connected family: inspect intended lineage and retained data; repair membership or disconnect the intended subtree transactionally. Never automatically merge Projects. -- Cross-organization lineage: resolve the workspace ownership/lineage first using the approved organization lifecycle operation. -- Changed family after dry-run: inspect the change, then produce and review a new plan. -- Empty Project or archive mismatch: determine the intended retained environments and lifecycle state. Do not silently delete Project data or unarchive environments. -- Cycle or missing parent: repair the fork connection before migration. - -The runner does not repair anomalies on the operator's behalf. After repair, run dry-run and verify again. Membership uniqueness and foreign keys remain active throughout. - -## Self-hosted execution - -Use the migration image from the same release as the application, after all application/worker replicas run compatible code. PostgreSQL 17+ and a direct session-capable primary connection are required. Supply `PROJECT_BACKFILL_DATABASE_URL` through your deployment's secret mechanism; do not place it in shell history or command arguments. Mount a writable, persistent report directory owned by the container's non-root user. - -Inside that image, the working directory is `/app/packages/db`: - -```sh -bun --no-env-file scripts/backfill-projects.ts identity -PROJECT_BACKFILL_REPORT_PATH=/reports/plan.json \ - bun --no-env-file scripts/backfill-projects.ts dry-run -PROJECT_BACKFILL_REPORT_PATH=/reports/apply.json \ - bun --no-env-file scripts/backfill-projects.ts apply \ - --from-file /reports/plan.json --database-id --writers-drained -PROJECT_BACKFILL_REPORT_PATH=/reports/verify.json \ - bun --no-env-file scripts/backfill-projects.ts verify -``` - -Override the migration container command for this one-off operation; do not change the ordinary `db:migrate` startup command. Apply the same report review, writer coordination, replay and activation rules above. Keep the original plan on persistent storage until verification and the release are complete. - -## Local proof - -`packages/db/scripts/project-backfill.integration.ts` creates a separate disposable database for every case, seeds a pre-Project fixture and applies the actual `0393` SQL. It exercises real SQL and the CLI, including interruption, conflicting writers, lost sessions, report failures and idempotent replay. The normal integration workflow discovers this file and uploads the integration JSON result. Never aim this test at an existing app database; the shared test-infrastructure validator restricts the target to a local test database. diff --git a/docs/plans/project-entity-foundation.md b/docs/plans/project-entity-foundation.md deleted file mode 100644 index f2442f8fb8b..00000000000 --- a/docs/plans/project-entity-foundation.md +++ /dev/null @@ -1,320 +0,0 @@ -# Project entity foundation: implementation and rollout proposal - -Backend foundation implemented locally, with backfill hardening validated on 2026-10-01, following the approved planning review. Final feature review and hosted rollout remain separate. Database validation uses disposable test containers only. See the implementation and rollout notes below for the concrete scope. - -Migration `0393_project_foundation` follows `0392_dashboard`. The schema expansion and application writers belong in the foundation release; backfill and contract enforcement follow separately. - - -## Decisions and blockers - -Settled: Project and environment are separate identities; existing workspace IDs remain environment IDs. Issues belong to Project across environments. Project and environment names are independent. Workflow, table, file, knowledge and dashboard data remain environment scoped. Chat has no Project ownership or membership relation. Selecting a Project is navigation state, not execution authority. Ship backend/feature PRs before new UI. - -The user settled Project visibility and Issue access: any environment access makes the Project visible; the toggle lists only accessible environments. Issues are project-wide and all-or-nothing, available by default to anyone with access to any environment. Through permission groups, an org admin can disable all Issues for a Project for partial-access teammates (those lacking access to every environment in that Project). This replaces the earlier explicit Project-membership proposal for Issue access. - -1. **Access — settled:** derive navigation and default Issue access from current environment access. Apply the permission-group restriction only to partial-access teammates, for the target Project; no per-Issue or per-environment filtering of the Issue collection. Restricting Issues does not hide the Project or its accessible environments. Project rename/archive is allowed for org admins and workspace admins who administer every environment; Issue visibility alone does not grant administration. The partial-access Issue check excludes archived environments. -2. **Disconnect — settled:** disconnecting a fork moves it and all descendants into a newly created Project, atomically with unlinking the fork edge. The original Project retains its ID. Existing Issues stay in the original Project; the new Project starts empty. A user may explicitly ask Sim Chat/Mothership to migrate Issues later through an authorized operation, rather than coupling Issue migration to disconnect. -3. **Personal scope — settled:** Projects can exist without an organization. Organization deletion preserves each Project and all of its Issues together, retaining the Project ID and assigning an explicit personal owner; no Project/Issue cascade or replacement Project creation. - -Settled organization-deletion behavior: transfer each Project to an explicitly selected personal owner while retaining its ID and Issues, rather than splitting its Issues between former environment owners. Project archive cascades to every environment and workflow. An active Project must retain at least one active environment; users cannot individually delete/archive the last one. Whole-Project archive is the explicit exception, leaving an archived Project with archived environments, not an empty active Project. Cross-organization environment moves must explicitly separate the moved environment from the old Project or move the entire Project. Existing Issues stay with their original Project on fork disconnect; a later user-requested migration is a separate operation. These policies are particularly important for subscription cancellation and administrative organization deletion. - -No Project member table or grant backfill is required for the settled Issue-access model. Evaluate existing effective environment access and applicable permission-group policy at authorization time. Permission changes immediately affect access; do not materialize permanent Project grants. The Issue partial-access comparison uses non-archived environments, as confirmed by the user. Zero-active-environment Projects are permitted only in archived state, with their environment rows retained. Require at least one accessible active environment for ordinary active-Project access; use explicit archived-resource authorization for archive inspection/restore, never the empty all-environments predicate; do not treat an empty set as full access. External collaborators use their existing effective access, without introducing a new org-membership requirement solely for Issues. - - -Lifecycle behavior: existing Issues remain with the original Project after disconnect; explicit migration can be requested through Sim Chat/Mothership. Org admins or callers with admin on every environment may rename/archive. Archived environments are excluded from the Issue partial-access comparison. The separate all-environment administrative rule includes archived environments; the exclusion applies specifically to Issue access. - -Organization deletion — settled: preserve each Project and its Issues together and assign an explicit personal owner, matching the existing workspace-preservation pattern. Do this atomically with workspace detachment and organization deletion; do not rely on a bare SET NULL FK or infer authority from the billing payer. Detachment assigns the current organization owner as the Project lifecycle owner, matching workspace detachment. If the organization has no owner, the Project retains its required existing lifecycle owner. The FK prevents unresolved user ownership. Subscription lapse is non-destructive as well; it must never reuse a Project/Issue purge path. - -## Organization-deletion preservation contract - -Verified at this planning baseline: - -- `apps/sim/app/api/v1/admin/organizations/[id]/route.ts:302`: transaction calls detach, organization-resource cleanup enqueue, then org delete. Its deletion contract explicitly says workspaces survive; subscriptions referencing the org block deletion. -- `apps/sim/lib/workspaces/organization-workspaces.ts:426`: detach selects organization-mode workspaces without excluding archived rows, clears organization via payer transfer, sets mode to `grandfathered_shared`, and grants admin to the new billing account (org owner if present, otherwise workspace owner). This is not a workspace purge. -- `apps/sim/lib/organizations/resource-cleanup.ts`: captures directly organization-owned files/chats before FK cascade in a transactional outbox. It is not a blanket workspace-resource cascade. -- `apps/sim/lib/auth/auth.ts:1773`: Better Auth organization deletion is disabled. -- `apps/sim/lib/billing/webhooks/subscription.ts:140`: subscription dormancy calls workspace detach, retaining the org. Reusing this helper must not accidentally delete Projects/Issues when a subscription lapses. - -Enumerate affected Projects under the organization lifecycle lock before changing workspace organization/membership. Compare `project.organizationId` with canonical membership/workspace ownership, report inconsistent or mixed-tenant Projects, and include archived org Projects and report legacy empty-Project anomalies that a workspace-only join would miss. Resolve and validate an explicit personal owner for each Project, update ownership to personal, and detach its environments in the same transaction before the organization is deleted. Preserve Project IDs, membership rows, Issues and Issue attachments; do not enqueue them for org-resource cleanup. Existing direct org chat/file cleanup remains separate. Retire organization-specific permission-group bindings explicitly after scope transfer; personal access then uses current effective environment access under the agreed policy. Do not copy grants or restrictions from unrelated orgs. A transfer must not leave a surviving Project referencing a deleted group or organization. Record old/new ownership and audit the actual operator, not the new owner or payer. Restrictive ownership FKs force the lifecycle operation to complete explicitly; they are not a reason to purge data. - -## Column-by-column schema proposal - -The preferred schema below uses environment-derived access and supports the accepted personal Project model. All new identifiers use `generateId()` and text columns, matching workspace and organization IDs. IDs are immutable and independent of names, lineage roots and deployment environment. - -| Column | Proposal and reason | -| --- | --- | -| `project.id` | Text PK, application-generated UUID v4. Never reuse a workspace ID; never recompute after backfill. | -| `project.name` | Non-null text; trim, require 1–100 characters in domain validation and enforce compatible DB bounds/nonblank check. Allow duplicate names; IDs disambiguate. No slug or case-insensitive unique-name constraint without a product requirement. | -| `project.organizationId` | Nullable FK to organization, `ON DELETE RESTRICT`. Null only for explicitly personally owned Projects. An explicit lifecycle transaction transfers Projects before organization deletion. Organization transfer must include archived Projects and surface legacy empty-Project anomalies too. | -| `project.ownerId` (new) | Required user FK, `ON DELETE RESTRICT`, retained on both personal and organization Projects, matching the workspace lifecycle-owner model. Lifecycle ownership, not implicit access. Anchors lifecycle ownership independently of environment owners; individual removal of the last environment is prohibited. Personal-owner deletion transfers or explicitly deletes the Project before user deletion. | -| `project.archivedAt` | Nullable timestamp; setting it archives the Project and all member environments/workflows through one compound operation. Preserve archived environment rows and Issues. Archived detail remains readable to authorized members; normal lists omit it unless requested; Project/Issue mutations reject except restore or approved cleanup. | -| `project.createdAt` | Non-null timestamp default now, immutable. Backfill records actual creation time; retain existing environment history rather than pretending the Project existed earlier. | -| `project.updatedAt` | Non-null timestamp default now; explicitly set by Project metadata/ownership/archive writes. `defaultNow` alone is not an update trigger. Environment membership changes are audited and update the affected environment timestamp; policy changes update their policy records. | -| root/default environment, icon/color, pipeline position, billing fields | Omit from foundation. None is needed for Issue identity or authority. A default execution environment, if added, must still be independently authorized. | - -Indexes: organization list index on `(organization_id, archived_at, id)`; personal-owner list index on `(owner_id, archived_at, id)`; sort by stable ID cursor initially, avoiding a name-based cursor that changes on rename. Add a name-sort index only if the selected list contract requires it. Foreign-key referencing columns need indexes for ownership/deletion lookup. Do not duplicate a simple organization index without demonstrated need. - -| Environment membership field | Proposal | -| --- | --- | -| `project_workspace.projectId` | Non-null text FK to Project. Prefer `ON DELETE RESTRICT` while environments remain, so deleting a Project cannot silently orphan environments. Changing membership on disconnect is an explicit transaction. | -| `project_workspace.workspaceId` | Non-null workspace FK with cascade on workspace deletion; unique index enforces at most one Project per environment. Use a composite PK `(projectId, workspaceId)` plus unique workspace index; Project-leading PK supports member lookup. | -| `project_workspace.createdAt` | Non-null timestamp default now, recording membership-row creation. On reassignment retain creation time and audit the move; introduce assignment timestamp only if required by an actual consumer. | -| `workspace.forkedFromWorkspaceId` | Keep existing lineage edge and `ON DELETE SET NULL`. Explicit disconnect creates a new Project; implicit edge removal on hard parent deletion does not silently move Issue ownership. | -| `workspace.organizationId` | Retain for compatibility/resource authorization. Proposed same-organization invariant with Project uses null-safe comparison and shared transactional scope changes; deferred constraint triggers validate final state across Project, membership and workspace writes. | - -**Membership storage:** a direct non-null `workspace.projectId` would enforce exactly-one membership more simply, but the accepted design uses `project_workspace`; do not implement both. The unique workspace index only enforces at most one. Add deferred constraint triggers on workspace insert and membership deletion/update to require one membership for every surviving workspace at commit after rollout. Lock/recheck the workspace when reassignment can race; deletion must allow cascaded removal for a workspace that no longer exists. Ship this enforcement only after compatible writers and backfill are complete. Require at least one environment membership for each Project and at least one active environment for each active Project. Archived Projects retain member rows but may have no active environments. Complement application checks with deferred final-state constraints after rollout; source and destination Projects must be checked on membership moves. - -Do not add `project_member` for navigation or Issues. Extend the existing permission-group model with a Project-targeted restriction on Issue access for partial-access teammates. Its absence means no extra restriction. Resolve the Project organization canonically; only its org administrators may configure it. Reuse existing group assignment and policy-composition rules instead of inventing a per-user grant table. The current resolver in `lib/permission-groups/config-scope.server.ts` is workspace-keyed; add intentional Project scope rather than passing a selected/root environment. Exact storage follows the permission-group skill during implementation. Project rename/archive authority is org admin OR admin on every environment; do not infer admin from an arbitrary environment. Never grant administration through an empty all-environments check. Active empty Projects are invalid; archived-resource operations authorize explicitly against retained environment grants and org administration. - -## Membership and lifecycle invariants - -1. Every environment, including archived ones, belongs to exactly one Project after enforcement. Each Project contains at least one environment; active Projects have at least one active environment. Whole-Project archive permits zero active environments while retaining archived member rows. -2. Environment and Project organization scopes agree. Personal environments within a Project may have different lifecycle owners only if one explicitly selected Project owner governs Project lifecycle; that must not transfer environment ownership or grant access. -3. Fork creation inherits its parent's Project in the same transaction after rechecking current source scope under lock. Existing fork families seed Projects initially. Explicit disconnection splits the subtree into a new Project. Multiple roots can still arise through parent deletion; do not infer Issue movement from root counts alone. -4. Nested forks inherit the same Project; siblings need not have a unique depth. A disconnected fork and every descendant move together to a fresh Project. Never manufacture a new Project from a missing-parent/backfill race. Reevaluate Issue eligibility on both Projects after the split: a previously partial-access user may become full-access. Project-targeted permission-group restrictions also need explicit migration semantics; proposed preserve applicable restrictions on the new Project rather than accidentally resetting configured policy, while acknowledging the all-or-nothing rule now applies to each new environment set. Record policy handling and both Project IDs in the operation audit. Existing Issues stay with the original Project. Explicit user-requested migration through Sim Chat/Mothership must authorize source and destination Projects under one semantic application operation, with atomic/retry-safe movement and audit; it is an Issues feature, not a Project-foundation dependency. -5. Renaming an environment does not rename the Project; rename the Project through its own operation. Backfill and implicit Project creation through legacy workspace creation use ` - Project`; explicit Project creation accepts independent Project and initial environment names. Workspace names remain untouched. Bound the generated Project name without losing its suffix, with `Untitled` for an empty source; surface any length normalization in the report. Duplicates are allowed and recorded, not merged. -6. Individual environment delete/archive must leave at least one active environment in an active Project and at least one member environment overall. Block removal of the last environment; direct hard-delete, account deletion, moves and fork disconnect cannot bypass the invariant. Serialize on the Project so two concurrent removals cannot each see the other as the remaining environment. For disconnect/move, validate both resulting Projects; refuse a split that strands the original as active with no active environment. Coordinated whole-Project lifecycle operations are the explicit exception for active-count checks, not an adapter flag that callers can freely set. -7. Project archive is one authorized compound operation that archives every environment and workflow, including operational deactivation already owned by existing archive paths. Retain membership rows and Issues. Reject new environment/fork/workflow admission into an archived Project. Preserve pre-existing archived states. Current `lib/workspaces/lifecycle.ts` commits workspace/resource archival before calling `archiveWorkflowsForWorkspace`; looping this helper across environments would permit a partially archived Project. Refactor transaction-owning primitives so durable Project/environment/workflow state changes commit atomically, with existing required external effects queued/retried after commit. Failure must not be reported as complete while workflows remain active. Project restore is deferred until a complete inverse exists; do not ship a metadata-only unarchive that leaves no active environments or silently revives resources archived before the Project. Archival does not imply cancellation of already-running executions beyond existing archive semantics unless separately specified. -8. Moving one environment between organizations cannot retain its old Project membership. For initial shipment, refuse a partial Project transfer unless the operation explicitly names a destination/new Project and defines the Issue outcome; preserve original Project Issues. A whole-Project transfer requires authorization and disclosure for all environments, retained Project data, collaborators and Issues. -9. Organization cancellation/detach/deletion must process Projects and all member environments atomically with existing payer transfers. The legacy per-owner workspace detach cannot silently determine how to split Project-wide data. If a complete, authorized transfer cannot be determined, stop before committing rather than silently orphaning or broadening access. -10. Project hard delete is not required to unblock Issues. Restrict it while environments or Issues exist. `issue.projectId` should be NOT NULL with restrictive deletion until an explicit purge policy is implemented. Deleting an account must account for personally owned Projects and retained Issues, not merely surviving workspace grants. - -## Authorization and application surface - -Provide a shared canonical Project application context: `{ projectId, organizationId, ownerId, archivedAt }`, loaded by ID, with optional asserted organization/workspace scope compared to canonical data. Return not-found for hidden/mismatched scope, consistent with shared concealment policy. Loading must be inside the authorized application-use-case lifecycle; an HTTP/tool adapter may not query protected membership itself. - -Create a small shared Project authorization wrapper alongside the existing workspace/organization wrappers. It must reject unsupported Principal kinds before loading protected data; authorize current effective environment access and applicable Project-scoped permission-group policy; distinguish Issue use from Project administration; respect archive state and typed errors; project semantic audit from authoritative results; and keep external side effects after success. Mutations lock and recheck Project state/authority sufficiently to serialize with environment access revocation, environment-set changes, policy changes, organization transfer and archive. Persist a durable audit/outbox record inside the transaction where existing shared infrastructure supports it; do not report a committed mutation as absent because a later notification fails. - -Do not use `defineAuthorizedWorkspaceUseCase` with an arbitrary root environment. Staging has both `defineWorkspaceOperation` and `defineOrganizationOperation`; the latter currently accepts session/organization-delegated principals and cannot model personal Project scope alone. Extend the shared foundation intentionally, rather than hiding authorization in `projects.ts` or borrowing a payer identity. - -Proposed first contract (session surface; supported principal expansion is explicit): - -| Operation | Authority and behavior | -| --- | --- | -| `projects.list` | Paginated Projects containing an accessible environment, in requested owner scope; archived Projects require a separate explicitly authorized archive listing, never vacuous environment-access checks; archived filter. No inaccessible environment IDs, names, counts or fork parent references. | -| `projects.get` | Basic navigation metadata visible through an accessible environment (archived-resource access uses a separate explicit policy); Issue and administrative capabilities checked separately; stable `{ id, name, organizationId, archivedAt, createdAt, updatedAt }` plus caller capabilities. | -| `projects.resolveForWorkspace` | Authorize environment access; it permits resolving basic Project navigation metadata through the canonical `project_workspace` membership, without requiring a separate Project grant. Never accept a supplied Project ID as truth. | -| `projects.listEnvironments` | Project navigation visibility plus each environment's existing access policy; filter hidden parents in lineage output. Paginate rather than embedding all environments in every Project result. | -| `projects.create`, `rename`, `archive` | Creation atomically creates the first environment under existing creation/quota policy. Rename/archive require org admin or admin on every environment; archive cascades to all member environments/workflows. Do not automatically add member grants. No zero-environment creation endpoint. Restore is deferred until full environment/workflow lifecycle behavior is defined. | -| `projects.addEnvironment` | One compound use case authorizing Project administration and existing workspace-creation policy, then atomically creating environment with correct Project membership. Legacy workspace creation still creates its own Project. | -| Project Issue restriction configuration | Existing permission-group application operations extended with Project scope; org-admin authorization and canonical group/Project organization matching. No separate Project membership CRUD. | -| Issue operations | Use the all-or-nothing Issue gate below; action-specific mutation policy remains to be agreed without introducing partial Issue visibility; canonical Issue lookup verifies `issue.projectId`. No environment ID required for Issue CRUD. Links to environment resources are individually authorized on resolution. | - -Issue authorization for a human caller uses the complete canonical environment set E and the subset A for which the caller currently has effective access. The server must compute E independently of the filtered environment-toggle response: - -```text -canViewProject = |A| > 0 -hasPartialEnvironmentAccess = |A| > 0 AND |A| < |E| -canUseProjectIssues = canViewProject - AND NOT (hasPartialEnvironmentAccess AND effectiveGroupPolicyDisablesIssuesForThisProject) -``` - -The agreed truth table is: no accessible environment -> no default Issue access; some environments -> all Issues by default, none when the restriction applies; all environments -> the partial-access restriction does not deny Issues. Normal authentication, principal-kind and archive/action rules still apply. Return Issue capability separately from navigation capability. Never return hidden environment names/IDs/counts merely to explain a denial. - -Enforce this once in the shared Project Issue authorization path used by list/detail/mutations, search, counts, notifications/subscriptions, exports and tools. A denied caller must not receive Issue titles or other Issue data through side channels. Existing environment resources linked from Issues still require their own environment checks; access to all Issues never grants production access. Ordinary Issue mutation roles belong to the Issues implementation, but may not create a per-environment Issue subset. Project rename/archive uses org-admin OR all-environment-admin authority; permission-group edits remain org-admin operations. - -Creation/deletion/archive/restoration/move of environments and access grants/revocations can change partial/full status. Reevaluate on the server and invalidate relevant capability/policy caches and live subscriptions. Serialize mutating checks with changes that can alter the decision. Do not select the policy using whichever environment the user toggled to. Missing policy defaults to allow, but a policy lookup failure must propagate as an error, not be interpreted as an absent restriction. Archived environments are excluded from this Issue access comparison, as confirmed by the user. - -Start Project HTTP routes at `/api/projects` and `/api/projects/[id]`, with named contracts under `lib/api/contracts/projects.ts` and shared builders. Add environment lookup and permission-group configuration routes as needed by the first consumer. Query hooks use `requestJson`, named contract types and React Query. Start with a session-only Project API; do not advertise unsupported key/delegation support. - -Workspace API keys and workspace-scoped executor/system principals **must not** gain project-wide Issue access just because their environment points at the Project. Personal API keys/OAuth need an explicit Project credential policy (there is no unambiguous environment `allowPersonalApiKeys` switch at this scope), scope enforcement and permission-group checks before v2 Project routes ship. Organization delegation already exists, but is documented/search-scoped; extend trusted audience/operation policies and recheck current subject membership before admitting Project operations. Org chat Project selection is a requested target only. No chat-to-Project relation, implicit default environment, or copied grant. - -The Issues implementation can build on the foundation contract. Existing environments become usable for Issues only after validated backfill/enforcement; new properly provisioned Projects can be exercised earlier behind a release gate. No promise of a production-ready ID resolver that invents IDs on reads. - -## Required code-path inventory - -Paths below are relative to the repository root. `Migrate` means membership/authorization/lifecycle behavior is in scope; it does not mean copying prototype implementations wholesale. - -| Path / ingress | Classification and required work | -| --- | --- | -| `packages/db/schema.ts`, `migrations/`, `migrations/meta/_journal.json` | Migrate: additive Project/environment-membership schema and Project-scoped permission-group restriction, later constraints; preserve staging's dashboard migration. | -| `packages/db/scripts/migrate.ts`, `script-migrations/index.ts`, `script-migrations/types.ts` | Migrate integration: SQL runs before registered scripts under a session migration lock. This lock does not stop application writers. Do not put a large unbounded backfill in startup. | -| `apps/sim/lib/workspaces/create.ts` | Migrate both `createWorkspaceInTransaction` and wrapper; Project, membership and workspace created atomically. Its `createDefaultPersonalWorkspaceInTransaction` covers enterprise owner-claim provisioning. | -| `apps/sim/app/api/workspaces/route.ts` | Migrate normal POST and lazy/default workspace creation through a shared authorized application creation operation; preserve legacy wire behavior and existing workspace URLs. | -| `apps/sim/lib/workspaces/application/create-organization-workspace.ts`; `lib/mothership/application/execute-organization-workspace-use-case.ts`; `lib/mothership/tools/server/workspaces.ts` | Migrate shared lower creation path; keep Mothership a trusted adapter and preserve org pinning, capabilities, billing admission and resource refresh behavior. | -| `apps/sim/lib/billing/enterprise-owner-claim.ts` | Migrate inherited transaction behavior; no nested independently committing Project creation. | -| `apps/sim/ee/workspace-forking/lib/create-fork.ts`; `application/`; `lib/lineage/{lineage,lineage-root,unlink}.ts` | Migrate Project inheritance/validation under existing rank-ordered lineage and row locks. Disconnect atomically creates a new Project and reassigns the complete descendant subtree. Repeated/concurrent unlink must not create duplicate Projects; preserve existing edge-lock ordering and reread the closure. Do not add an organization lock held across the entire copy without reconciling lock order. | -| `apps/sim/app/api/workspaces/[id]/route.ts`; `lib/workspaces/lifecycle.ts` | Migrate required use-case boundaries; environment rename remains independent, DELETE is archive; guard last-active-environment removal. Add Project-wide atomic archive primitives while preserving existing resource archival/deactivation semantics and retryable effects. | -| `apps/sim/lib/workflows/lifecycle.ts` | Account-related bulk workspace archive must obey Project last-environment invariants; full Project archival must enlist workflow state in the same compound operation. Transfer surviving Projects on account deletion or use an explicit whole-Project lifecycle path. Workflow restore is not workspace restore. | -| `apps/sim/lib/workspaces/organization-workspaces.ts` | Migrate attach/detach closure, locks, Project scope and personal-owner transfer together with existing payer changes; include archived environments/Projects and report legacy empty-Project anomalies. | -| `apps/sim/lib/invitations/core.ts`; `lib/billing/organization.ts`; `app/api/v1/admin/organizations/[id]/members/route.ts`; `scripts/consolidate-users-into-organization.ts` | Required attach callers. Reuse corrected transaction primitive; recompute Project visibility/Issue eligibility after environment grants or scope changes. Do not broadly refactor v1 APIs. | -| `apps/sim/lib/billing/webhooks/subscription.ts`; `app/api/v1/admin/organizations/[id]/route.ts` | Required detach/cancellation/delete callers; explicit Project lifecycle before restrictive org FK. Preserve atomic detach+delete and billing semantics. | -| `apps/sim/lib/workspaces/admin-move.ts`; `admin-move-source-impact.ts`; `lib/admin/member-operation.ts`; `lib/billing/enterprise-provisioning.ts`; `app/api/v1/admin/dashboard/workspaces/[id]/move/route.ts` | Migrate transfer preflight/disclosure, Project conflicts and transaction ownership. Existing moves can dissolve fork edges; that alone cannot define Project transfer. Keep admin operation receipts and recoverable audit. | -| `apps/sim/lib/billing/organizations/membership.ts` | Migrate member removal, external removal, member transfer and organization-owner transfer to invalidate Project Issue eligibility and handle personal lifecycle ownership alongside workspace permission rows. | -| `apps/sim/lib/users/account-deletion.ts` | Migrate deletion planning/rechecks/transaction for retained Issue data, owned Projects and last-environment guards, transfers and Project FK restrictions. Direct workspace deletion uses alias `workspaceTable`; a search only for `delete(workspace)` misses it. | -| `apps/sim/lib/workspaces/{list,public-queries,host-context,seed-workspace-list}.ts`; `application/list-*.ts`; `packages/platform-authz/` | Preserve existing environment authority. New Project reads must not replace resource scope or turn Project membership into environment permission. Add owner-context resolution only where needed. | -| `apps/sim/lib/workspaces/application/{manage-permissions,remove-member}.ts`; `permissions/`; `access/workspace-access.ts`; invitations and organization member operations | Required access inventory. Reevaluate any/all environment access at invite/removal and update live Issue access; do not materialize permanent Project grants. | -| `apps/sim/app/api/v2/workspaces/**`; `lib/api/contracts/v2/`; `lib/api/mcp/generated/v2-operations.ts`; SDK/CLI generation | Preserve existing environment API. Project APIs are an additive surface after credential policy, using the same Project application operations. Regenerate descriptors only when adding that surface. | -| `apps/sim/app/api/v1/admin/workspaces/[id]/{import,export}/route.ts`; `lib/workflows/operations/import-export.ts`; `lib/workspaces/__integration__/mapped-import.integration.ts` | Reviewed scope: import works into an existing environment, not a new Project. Preserve identity and never import Project grants/IDs from resource payloads. No broad v1 migration. | -| `apps/sim/background/cleanup-soft-deletes.ts`; `lib/billing/storage/{payer-transfer,tracking}.ts`; `lib/mothership/inbox/{lifecycle,settings-store}.ts` | Reviewed adjacent writers: resource cleanup, payer/accounting and inbox settings are not Project identity changes. Project archive must invoke established environment/workflow deactivation; preserve existing retention policy and do not add immediate hard purge. No blanket edits to every workspace update. | -| `apps/sim/lib/permission-groups/{config-scope.server,resolve.server,fields,mutation}.ts`; `application/`; group contracts/settings UI | Extend Project-scoped partial-access Issue restriction through existing authorized group operations. Default absent setting to allow; preserve other fields on update; resolve all applicable groups using existing composition rules. Validate group/Project organization and invalidate Project Issue capabilities on edits. Read `add-permission-group-item` before implementation. | -| New `apps/sim/lib/projects/application/**`, contracts, internal routes, hooks | Implement fresh against staging shared foundations. Session operations first; add other adapters only with intentional Principal policy. | -| Prototype `lib/mothership/chat/lifecycle.ts`, `chat/application/fork.ts`, `chat/list-mothership-chats.ts` and `copilot_chat_project` | Non-goal: exclude chat relation imports, writes, filters, backfill and schema. Separate chat-ownership work removes obsolete prototype relations if ever deployed there. | -| Prototype `app/o/[organizationId]/p/hooks/use-projects.ts` and resource-browser/UI port | Defer: consume true Project IDs only after foundation. Extract final behavior later; no prototype cherry-picks. | - -Implementation must repeat the write-ingress search after rebasing (including schema aliases/raw SQL, user cascades, scripts and fixture writers). This is a source-based inventory, not a claim that production rows already satisfy the invariants. - -## Backfill and deployment procedure - -### Expand and capture a plan - -1. Ship additive Project and membership tables without enforcing membership completeness yet; keep current UI/API functional. Do not rewrite workspace names, routes, resource IDs or chat rows. Ship compatible writers in the same foundation release, but account for old replicas during rollout. Backfill activation and final constraints are separate deployment steps. -2. Use `packages/db/scripts/backfill-projects.ts`, with explicit `PROJECT_BACKFILL_DATABASE_URL`, `PROJECT_BACKFILL_REPORT_PATH`, and `dry-run` / `apply --from-file --database-id --writers-drained`. Run with `bun --no-env-file` so a checkout’s `.env` cannot select the database. The operator must verify the supplied target; there is no implicit application-DSN fallback. -3. Scan all workspaces, including archived, in stable ID keyset pages. Discover graph components with cycle detection and bounded traversal; roots are rows with no parent, missing parents are reported, cycles and organization mismatches are blocking anomalies. Keep only one bounded family in memory. Committed Project and membership rows serve as the durable resume mapping. Never silently reconnect already detached lineages or group unrelated roots by equal name. -4. Group existing environments by their current fork connections. A root workspace and all forks still connected beneath it become one Project. For example, Production → Staging → Dev becomes one Project containing all three environments. If Staging was previously disconnected from Production, create two Projects: one for Production, and one for Staging plus Dev. A workspace with no connected forks gets its own Project. Each new Project uses the root workspace’s name plus “ - Project” and its `ownerId`. - - Preserve any valid Project assignment that already exists. If only some environments in the tree have been assigned to one compatible Project, add the remaining environments to that same Project. If the tree is assigned to multiple Projects or spans multiple organizations, report the conflict and leave it unchanged for review; do not automatically merge it. - - If all environments in the tree are archived, create an archived Project and use the most recent environment archive timestamp. Otherwise, create an active Project. Never reactivate an environment during migration. An existing Project with no environments is reported for repair because every Project must contain at least one environment. - -5. Allocate each proposed Project ID in the completed dry-run artifact with `generateId()`. Apply consumes that artifact and validates its database identity and family fingerprints. Reruns use the same planned IDs, including after an interrupted apply; compatible existing assignments are reused. No extra manifest table is required. -6. Duplicate names are allowed; bound and trim seed names deterministically and record before/after. An ID collision fails the batch; never silently replace a planned ID during apply. Never overwrite an unrelated Project on collision. No Project-member/grant backfill; existing groups gain an absent/default-allow restriction without changing existing settings. No chat backfill, no Issue backfill in this PR. - -### Coordinate writers and commit batches - -7. Before apply, drain old app/worker/script writers or use a bounded write-maintenance window for the affected lifecycle operations. The migration runner lock serializes migration runners only. A one-shot script registered in the startup runner may finish before old replicas stop creating unassigned environments and is not a readiness signal. -8. New create/fork/attach/detach/move writers and the backfill must share a documented lock order. Integrate with existing organization, invitation, payer, lineage and workspace locks; do not introduce an opposite Project→organization order. Lock scope owners/Project/family according to that rank plan, then member rows in stable code-unit ID order, re-read closure and fingerprints under lock, and retry if scope changed. For fork versus bulk attach/detach, the closure must be re-read after serialization so a newly committed child cannot be omitted. If online closure coordination cannot be proved, use the maintenance window for those operations. -9. Each normal family commits Project + all environment assignments atomically. Insert membership only for unassigned workspaces; a unique-workspace conflict requires a locked reread and validation of the expected Project, not silent success. Do not ignore unique conflicts. Keep counters/report progress after commit; counters must describe committed work. -10. Bound batches by rows and transaction duration, not only 200 roots. Oversized families are explicitly reported and processed in a dedicated controlled transaction/window, or through a staged assignment design whose incomplete state is hidden from Project consumers. Never expose a partially migrated Project as complete. Resume from the original dry-run plan and committed memberships; recover committed batches after process death without new IDs or accidental policy restrictions. -11. Keep legacy workspace UI operational while Project/Issues reads are release-gated until readiness. During rolling deployment a missing membership is an explicit not-ready state, not permission to create a new Project on a GET. A compatible fork writer must ensure its parent family transactionally, or temporarily refuse fork creation for an unassigned family. - -### Validate and enforce - -The command must emit counts and offending IDs for at least these checks (illustrative SQL for the proposed schema): - -```sql --- Must be zero before exactly-one enforcement. -SELECT w.id FROM workspace w -LEFT JOIN project_workspace pw ON pw.workspace_id = w.id -WHERE pw.workspace_id IS NULL; - --- Must be zero; use null-safe comparison for personal scope. -SELECT w.id, pw.project_id FROM workspace w -JOIN project_workspace pw ON pw.workspace_id = w.id -JOIN project p ON p.id = pw.project_id -WHERE w.organization_id IS DISTINCT FROM p.organization_id; - --- Existing fork edges must stay in the same Project. -SELECT child.id FROM workspace child -JOIN project_workspace child_pw ON child_pw.workspace_id = child.id -JOIN project_workspace parent_pw ON parent_pw.workspace_id = child.forked_from_workspace_id -WHERE child_pw.project_id IS DISTINCT FROM parent_pw.project_id; - --- No active Project may lack active environments. -SELECT p.id FROM project p -WHERE p.archived_at IS NULL AND NOT EXISTS ( - SELECT 1 FROM project_workspace pw JOIN workspace w ON w.id = pw.workspace_id - WHERE pw.project_id = p.id AND w.archived_at IS NULL -); - --- Even archived Projects retain at least one environment row. -SELECT p.id FROM project p WHERE NOT EXISTS ( - SELECT 1 FROM project_workspace pw WHERE pw.project_id = p.id -); - --- Every Project retains a user lifecycle owner. -SELECT id FROM project -WHERE owner_id IS NULL; - -``` - -Additionally validate FK orphans, cycles/missing parents, duplicate/partial manifest assignment, permission-group Project/organization mismatches, counts of archived environments, empty Projects, source permission changes since planning, and no Project membership implied by chat data. Empty Projects and active Projects with no active environments are blocking anomalies; repair them explicitly, never silently delete retained Issues. Duplicate names and fully archived Projects with retained environments are valid. Check plan/report counts against actual committed rows and ensure every blocked family is resolved before completion. - -12. After compatible writers are fully deployed and validation passes, add/validate constraints, enforce exactly-one membership, and enable consumers. Use staged `NOT VALID` validation for existing-row constraints where appropriate; lock-time-bounded DDL and staging's documented concurrent-index mechanism for large workspace indexes. A cross-table deferred trigger must validate both workspace and Project ownership updates and serialize conflicting writes; test concurrent transactions, not just final-state SQL. Ordinary FKs still own referential existence. -13. Repeat validation after a normal write interval. Confirm a fresh install and an upgraded self-hosted database reach the same ready state. An operator-run production backfill needs a documented self-hosted upgrade path; readiness must remain false until it completes. Do not silently hang all deploys on a production-sized graph scan. - -### Recovery and rollback - -Before enforcement/consumer activation, application rollback may ignore additive schema, but old writers require pausing the backfill and another reconciliation before reactivation. After exactly-one membership enforcement, rolling back to old writers is unsafe: they cannot create environments. Roll back only to the compatible foundation release, or deliberately relax the constraint with writes gated and rerun reconciliation afterward. - -Keep IDs, membership rows, and operator reports durable. If a backfilled grouping is wrong before Issues begins, correct it transactionally from the recorded mapping with an explicit reviewed repair. Once Issues reference a Project, do not undo by dropping Projects or regenerating IDs; use a forward repair with an explicit Issue retention/move decision. Preserve backups and reports. No automatic destructive down migration. Retry failed batches; committed family assignments survive. A script runner's completed-name marker is not a substitute for a row-level readiness check. - -## PR and dependency sequence - -| PR | Scope and dependency | Deploy boundary | -| --- | --- | --- | -| A — Project foundation | Clean implementation branch from fresh staging; schema expansion, environment-derived Project/Issue access and Project-scoped partial-access restriction, stable IDs and session contracts, shared creation/fork writes, required lifecycle compatibility, dry-run/apply tooling and focused proofs. No chat/UI. | Additive deployment; existing workspace UI works; Project consumer gate initially off. Project/environment placement are atomic. Required org/user lifecycle handling must not be deferred past activation. | -| B — migration enforcement | Depends on A: bounded apply runbook, validated data/readiness, constraints and activation. Can be a small separate PR/release, or a second deployment phase of A. | Must follow compatible-writer rollout and measured validation. No hardcoded success flag standing in for data integrity. | -| C — Issues | Issues stacks on A after access/lifecycle contract is agreed; merges against A and activates after B. Issue FK, application operations, routes and any feature UI independently reviewable; enforce the partial-access restriction across every Issue surface before enabling it. | Issue identity is Project ID; no environment-owner authorization fallback. | -| D — chat ownership | Independent of A/B/C. Org/personal chat ownership and resource browsing, with obsolete prototype Project references excluded. | No chat-to-Project relation or Project migration dependency. | -| E — remaining backend features | Resource-browser APIs, environment management and other feature foundations as separate PRs. Dashboards already exist in staging; do not reintroduce prototype migration. | Depends on A/B only where actual Project operations are used. | -| F — new organization/Project UI | Built on landed feature surfaces; extract completed behavior from the aggregate experiment. True Project ID and selected environment ID remain distinct. | Last; current workspace navigation remains supported until replacement is deliberately shipped. | - -If A becomes too large, split a prerequisite shared authorization/closure-lock fix from the entity PR, then stack A on it. Do not split out required lifecycle writes such that an enabled Project entity can silently corrupt on cancellation, invitation acceptance or account deletion. A schema-only merge can be additive, but is not the complete foundation required by Issues. - - -## Verification plan and release evidence - -Apply the `test-audit` authoring gate before code: enumerate failure modes, choose one strongest owner boundary, and avoid schema/registry/config assertions. Use disposable Postgres/Redis and an isolated app. Report to caller-supplied `PROJECT_FOUNDATION_REPORT_PATH` / `PROJECT_BACKFILL_REPORT_PATH` with each check's status, duration, failure and IDs; upload on CI failure. - -| Real boundary | Failure to prove | -| --- | --- | -| Migration against pre-Project DB snapshot | Active/archived singleton, siblings, nested forks, detached family, duplicate names, mixed owners, missing parent, cycle, cross-org family, prior partial/split assignments. Valid rows map once; anomalies block without mutation; rerun uses identical IDs and preserves default-allow policy. | -| Real concurrent DB transactions | Fork versus backfill; fork versus attach/detach/move; whole-Project archive versus fork/environment/workflow creation; two concurrent last-environment removals; archive versus Issue write; environment access revoke versus Issue write; full-to-partial transition versus Issue write; restriction edits versus cached reads; crash between batches. Assert committed scope and recoverability, not mocked call order. | -| Real create/provision/fork HTTP or application boundary | Workspace+Project rollback together after injected transactional failure; regular creation, first visit, enterprise claim and org Mothership creation all obey membership. | -| Real Project/Issue HTTP | Staging-only grant shows the Project and only staging in its toggle, with production hidden; partial-access user sees all Issues by default and none with the applicable group restriction; full-access user retains Issues under that restriction; no-environment user gains nothing; restriction changes do not hide accessible environments; list/detail/search/counts/notifications/tools honor denial; cross-org assertion concealed; workspace API key rejected; archived Project read allowed/write denied; active empty Project creation/removal is rejected; archived-state checks cannot grant access through an empty set; revocation effective without stale authorization cache. | -| Lifecycle E2E | Independent renames; disconnect creates one new Project for the complete subtree, retains original Project ID, and leaves existing Issues in the original Project; individual archive/delete of last environment is rejected; whole-Project archive covers every environment/workflow atomically and retains Issues; org cancellation/delete produces approved personal ownership or atomic refusal; user deletion handles retained Issues and last-environment invariants; transfer rollback preserves billing and scope. | -| Current UI compatibility | Existing workspace URLs, resource execution and import/export still work; no chat migration; Project selection cannot choose an execution environment implicitly. | -| Upgrade/install | Fresh install, interrupted backfill resume, rolling old/new writers before enforcement, enforced-release rollback boundary and self-hosted completion path. | - -Extend existing real suites where they own the failure: `lib/workspaces/__integration__/fork-sync.integration.ts`, `http-cli.integration.ts`, `mapped-import.integration.ts`, and `ee/workspace-forking/lib/lineage/fork-lock-order.integration.ts`; read their full coverage before deciding on additions. Project authorization and migration have new independent contracts and need real owner-boundary proofs. Existing type checks cannot prove concurrent ownership integrity. Demonstrate regressions go red with each relevant guard removed; never add a test that only restates a table definition. - -Implementation checks: relevant integration/E2E runs; workspace type-checks (app/db/auth/platform-authz as touched); `bun run check:api-validation:strict`; `bun run check:audits`; repository lint with review of autofixes; `git diff --check`. Local validation now includes 35 foundation/fork/lifecycle integration tests, 9 dedicated backfill/CLI integration cases, app/database type checks, lint, all 57 audits and diff checks. A negative control confirmed the changed-family guard catches a newly added descendant. These checks do not represent full coverage of every future-consumer scenario in the matrix above. No hosted backfill, browser rehearsal or production activation has occurred. - - -### Ownership clarification during implementation - -Every Project retains a required `ownerId`, including organization Projects. `organizationId` is optional; the two fields are not mutually exclusive. This matches workspace lifecycle ownership. Neither field substitutes for explicit workspace grants or organization-admin authority. A departing owner is transferred before account deletion; Project ownership does not cascade away with a user. Organization detachment keeps Project identity and explicitly assigns its personal lifecycle owner. - - -## Concrete implementation and rollout notes - -The schema contains `project` (`id`, `name`, required `ownerId`, nullable `organizationId`, `archivedAt`, `createdAt`, `updatedAt`) and `project_workspace` (`projectId`, unique `workspaceId`, `createdAt`). There is no stored fork depth, position, or Project billing state. - -For request/response examples and the distinction between explicit Project creation and existing workspace creation, see the [Project creation guide](../../apps/sim/lib/projects/README.md). - -Implemented session routes: - -- `POST /api/projects`: creates a named Project and its named first environment atomically, including admin permissions and a starter workflow. Requires explicit `organizationId` (or `null` for personal scope), `name`, and `initialEnvironment.name`; returns both IDs and names with HTTP 201. Uses existing workspace creation eligibility, billing and permission-group rules. -- `GET /api/projects`: authorized, cursor-paginated active Project inventory. -- `GET /api/projects/[id]`: metadata, accessible active environments, and administration/Issues capabilities. -- `GET /api/projects/by-workspace/[workspaceId]`: canonical lookup that also authorizes the requested environment. -- `PATCH /api/projects/[id]`: rename. -- `DELETE /api/projects/[id]`: compound Project/environment/workflow archive. - -Workspace creation creates its Project atomically; fork creation joins the existing Project. Existing workspace creation callers retain their response shape and generated Project names. Project creation shares their transaction primitive; it cannot attach an arbitrary environment or create an empty Project. A separate empty-Project create, arbitrary environment attachment, restore UI, public-key API, and Issues CRUD are not implemented here. The existing permission-group editor gains the Project Issues restriction; the new Project navigation UI remains a separate consumer. - -`deniedPartialAccessProjectIssues` is a Project-ID denylist on existing permission-group config. An empty list allows access. For a partial-access teammate, each accessible active environment resolves its effective group using existing explicit-member/all-members/default precedence. If any of those effective groups denies this Project, all Issues are denied. The selected environment cannot change the answer. Archived environments are excluded from this comparison. Full-access teammates bypass this partial-access restriction. Issue use cases call `authorizeProject` in their own transaction before reading or writing Issue rows; `getProjectIssueAccess` is only a read-only probe. The Issue gate holds the permission-group lock through that transaction, so callers must respect its existing leaf-lock rule. - -Disconnect copies a matching restriction to the new Project. Moving a Project out of an organization removes its stale ID from that organization’s group configs. Rename/archive authorization includes every environment, including archived members. A departing organization member’s Project lifecycle ownership moves to the organization owner. Account teardown transfers surviving Projects to an administrator of their surviving environments and explicitly removes wholly private Projects in the same teardown transaction; it cannot leave a surviving active Project with no active environment. - -The backfill scans 100 workspaces per page, limits ancestor traversal to 1,000 links and CLI family batches to 1,000 environments by default (10,000 maximum), and commits one family at a time. It reports cycles, missing parents, oversized families, cross-organization lineages, conflicting assignments and inconsistent archive states. A reserved session advisory lock excludes overlapping backfill runs. Apply takes bounded NOWAIT table locks before rediscovering and validating the full family. Organization moves use a nonblocking Project lock because their legacy paths can already hold workspace locks; contention returns a retryable conflict. A conflicting batch leaves prior committed families intact and is safe to rerun after repair. - -Operator sequence: - -1. Deploy the additive migration and compatible writers. Do not enable downstream Project/Issues consumers yet. -2. Run dry-run with an explicitly verified database target and inspect its report. -3. Drain old writers and pause lineage/ownership maintenance while applying. Pass `apply --from-file --database-id --writers-drained`; this flag is an operator assertion, not an automatic drain. Legacy unassigned workspaces remain usable, but their fork operation returns a backfill-required conflict until assignment completes. -4. Rerun and verify zero new assignments and zero conflicts, then execute the reconciliation queries in this plan. Resolve any remaining unassigned/empty/mismatched rows before activation. -5. Enable consumers only after reconciliation. Complete database-level membership/minimum-environment enforcement in a later deployment, as recorded by `contract-pending` in the schema. Do not activate consumers on old-writer rollback without another reconciliation. - -Example invocation from the repository root (supply the target URL through your normal secret mechanism): - -```sh -PROJECT_BACKFILL_REPORT_PATH=/absolute/path/project-backfill-dry-run.json \ -bun --no-env-file packages/db/scripts/backfill-projects.ts dry-run - -PROJECT_BACKFILL_REPORT_PATH=/absolute/path/project-backfill-apply.json \ -bun --no-env-file packages/db/scripts/backfill-projects.ts apply \ - --from-file /absolute/path/project-backfill-dry-run.json \ - --database-id --writers-drained -``` - -Validation artifacts come from `PROJECT_FOUNDATION_REPORT_PATH` (or `apps/sim/test-results/project-foundation.json`) and the integration runner’s JSON report. CI already uploads `apps/sim/test-results/*.json`. The former mock-count workspace lifecycle tests were replaced by real database checks of concurrent removal and retry repair. The dedicated backfill suite executes the exact additive migration in a disposable database before exercising backfill. External webhook/socket/MCP cleanup retains existing best-effort post-commit behavior; durable archive state commits atomically. - -The operator-run CLI procedure, self-hosted commands, SQLSTATE handling, lock behavior, report interpretation and repair procedure are documented in [the Project backfill runbook](./project-backfill-runbook.md). - -### Follow-up: Project-scoped files - -Project names provide the identifying label; this foundation intentionally omits a separate description field. Shared purpose, goals, and operating guidance belong in Project-owned files, with a designated brief identified by a stable document ID. - -Extend file ownership to Projects in a separate PR, likely with nullable `projectId` on the existing files table and constraints for valid scope combinations. Include authorization, version history/collaboration, storage lifecycle, and Mothership tools/VFS support. Keep environment files restricted to their environments; publishing them to a Project must explicitly account for broader readership. Project-scoped files and brief consumption are not implemented in this foundation. diff --git a/packages/db/scripts/backfill-projects.ts b/packages/db/scripts/backfill-projects.ts index 1512838d2d5..3dc9775b5e2 100644 --- a/packages/db/scripts/backfill-projects.ts +++ b/packages/db/scripts/backfill-projects.ts @@ -22,7 +22,12 @@ Commands: Required environment: PROJECT_BACKFILL_DATABASE_URL (direct primary connection). Reports: PROJECT_BACKFILL_REPORT_PATH (required except identity). -Apply requires --writers-drained and --database-id FINGERPRINT. +Apply requires --writers-drained --project-writers-enabled --release-revision REVISION +and --database-id FINGERPRINT. These are operator assertions, not fleet verification. +Deploy first with PROJECT_WRITES_ENABLED=false and PROJECT_API_ENABLED=false. +Drain old code, enable Project writers on every compatible instance, then backfill. +Keep the Project API off until verification and later contract enforcement. +Once Project data exists, never roll back to code that predates Project support. Options: --from-file PATH, --seconds 600 (1–3600), --max-family-size 1000 (1–10000). Run with compatible writers deployed. Pause lineage/ownership writes during apply. Short NOWAIT table locks exclude concurrent changes during each family transaction. @@ -38,6 +43,8 @@ async function main() { help: { type: 'boolean' }, 'from-file': { type: 'string' }, 'writers-drained': { type: 'boolean' }, + 'project-writers-enabled': { type: 'boolean' }, + 'release-revision': { type: 'string' }, 'database-id': { type: 'string' }, seconds: { type: 'string', default: '600' }, 'max-family-size': { type: 'string', default: '1000' }, @@ -78,13 +85,34 @@ async function main() { throw new Error('Invalid command') const reportPath = process.env.PROJECT_BACKFILL_REPORT_PATH if (!reportPath) throw new Error('Set PROJECT_BACKFILL_REPORT_PATH explicitly') + const releaseRevision = values['release-revision']?.trim() ?? '' if ( mode === 'apply' && - (!values['writers-drained'] || values['database-id'] !== databaseId || !values['from-file']) + (!values['writers-drained'] || + !values['project-writers-enabled'] || + !releaseRevision || + values['database-id'] !== databaseId || + !values['from-file']) + ) + throw new Error( + 'Apply requires --writers-drained, --project-writers-enabled, --release-revision, matching --database-id, and --from-file' + ) + if ( + mode !== 'apply' && + (values['writers-drained'] || + values['project-writers-enabled'] || + values['release-revision'] || + values['from-file']) ) - throw new Error('Apply requires --writers-drained, matching --database-id, and --from-file') - if (mode !== 'apply' && (values['writers-drained'] || values['from-file'])) throw new Error('Apply-only options used with read-only mode') + const operatorAssertions = + mode === 'apply' + ? { + oldWritersDrained: true, + projectWritersEnabled: true, + releaseRevision, + } + : undefined const inputPath = values['from-file'] if (inputPath && resolve(inputPath) === resolve(reportPath)) throw new Error('Keep the dry-run plan and apply report in separate files') @@ -118,7 +146,14 @@ async function main() { const partialPath = `${reportPath}.${process.pid}.partial` await writeFile( partialPath, - JSON.stringify({ databaseId, mode, completed: false, ready: false, stopped: 'Starting' }), + JSON.stringify({ + databaseId, + mode, + operatorAssertions, + completed: false, + ready: false, + stopped: 'Starting', + }), { mode: 0o600 } ) await rename(partialPath, reportPath) @@ -130,7 +165,9 @@ async function main() { maxFamilySize: Number(values['max-family-size']), shouldStop: () => interrupted, onProgress: async (progress) => { - await writeFile(partialPath, JSON.stringify(progress, null, 2), { mode: 0o600 }) + await writeFile(partialPath, JSON.stringify({ ...progress, operatorAssertions }, null, 2), { + mode: 0o600, + }) await rename(partialPath, reportPath) logger.info('Project backfill progress', { mode, diff --git a/packages/db/scripts/project-backfill.integration.ts b/packages/db/scripts/project-backfill.integration.ts index b60b960c7da..dc4a4c30e8c 100644 --- a/packages/db/scripts/project-backfill.integration.ts +++ b/packages/db/scripts/project-backfill.integration.ts @@ -344,6 +344,22 @@ describe('Project backfill CLI', () => { const dry = await run(['dry-run'], planPath) expect(dry.code, dry.stdout + dry.stderr).toBe(0) const plan = JSON.parse(await readFile(planPath, 'utf8')) as ProjectBackfillReport + for (const missing of [ + '--project-writers-enabled', + '--release-revision', + '--writers-drained', + ]) { + const args = ['apply', '--from-file', planPath, '--database-id', plan.databaseId] + if (missing !== '--writers-drained') args.push('--writers-drained') + if (missing !== '--project-writers-enabled') args.push('--project-writers-enabled') + if (missing !== '--release-revision') + args.push('--release-revision', 'integration-compatible-release') + const refused = await run(args) + expect(refused.code).toBe(1) + expect(refused.stdout + refused.stderr).toContain('Apply requires') + expect(await sql`SELECT * FROM project`).toHaveLength(0) + } + expect( ( await run([ @@ -351,6 +367,9 @@ describe('Project backfill CLI', () => { '--from-file', planPath, '--writers-drained', + '--project-writers-enabled', + '--release-revision', + 'integration-compatible-release', '--database-id', 'wrong', ]) @@ -364,11 +383,20 @@ describe('Project backfill CLI', () => { '--from-file', planPath, '--writers-drained', + '--project-writers-enabled', + '--release-revision', + 'integration-compatible-release', '--database-id', plan.databaseId, ]) ).code ).toBe(0) + const applied = JSON.parse(await readFile(reportPath, 'utf8')) + expect(applied.operatorAssertions).toEqual({ + oldWritersDrained: true, + projectWritersEnabled: true, + releaseRevision: 'integration-compatible-release', + }) expect((await run(['verify'])).code).toBe(0) const report = JSON.parse(await readFile(reportPath, 'utf8')) as ProjectBackfillReport expect(report.ready).toBe(true) @@ -380,6 +408,9 @@ describe('Project backfill CLI', () => { '--from-file', planPath, '--writers-drained', + '--project-writers-enabled', + '--release-revision', + 'integration-compatible-release', '--database-id', plan.databaseId, ], From c1c8e5da95f2d9572240866483c27a02f22a4190 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 16:34:24 -0700 Subject: [PATCH 6/8] refactor(projects): prepare compatible writers for the SQL backfill --- .../lib/billing/organizations/membership.ts | 2 +- apps/sim/lib/core/config/env.ts | 1 - apps/sim/lib/projects/README.md | 6 +- .../__integration__/foundation.integration.ts | 124 ++--- apps/sim/lib/projects/account-deletion.ts | 9 +- apps/sim/lib/projects/membership.ts | 22 +- apps/sim/lib/projects/rollout.server.ts | 11 +- apps/sim/lib/workspaces/create.ts | 34 +- packages/db/package.json | 4 - packages/db/project-backfill.ts | 471 ------------------ packages/db/scripts/backfill-projects.ts | 205 -------- .../scripts/project-backfill.integration.ts | 427 ---------------- scripts/test-integration.ts | 12 +- 13 files changed, 103 insertions(+), 1225 deletions(-) delete mode 100644 packages/db/project-backfill.ts delete mode 100644 packages/db/scripts/backfill-projects.ts delete mode 100644 packages/db/scripts/project-backfill.integration.ts diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index ad406b11e1e..220b0dba82d 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -557,7 +557,7 @@ async function reassignOwnedOrganizationResourcesTx({ const ownerId = ownerMembership?.userId if (!ownerId || ownerId === userId) return 0 - await lockProjectBackfillWrites(tx) + await lockProjectBackfillWrites(tx, workspaceIds) const ownedProjects = await tx .select({ id: project.id }) .from(project) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 2fa5809732f..9904b0143e3 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -669,7 +669,6 @@ export const env = createEnv({ // SSO Configuration (for script-based registration) SSO_ENABLED: z.boolean().optional(), // Enable SSO functionality - PROJECT_WRITES_ENABLED: z.boolean().optional(), // Activate new Project assignments after old writers drain PROJECT_API_ENABLED: z.boolean().optional(), // Expose Projects after backfill and contract enforcement SCIM_ENABLED: z.boolean().optional(), // Enable SCIM directory provisioning USAGE_MONITORING_ENABLED: z.boolean().optional(), // Enable organization usage monitoring on self-hosted (bypasses hosted requirements) diff --git a/apps/sim/lib/projects/README.md b/apps/sim/lib/projects/README.md index fb81f7dbb26..88c3a330a9f 100644 --- a/apps/sim/lib/projects/README.md +++ b/apps/sim/lib/projects/README.md @@ -2,14 +2,14 @@ A Project groups environments. An environment is an existing `workspace` record; there is no separate environment table. Every newly created Project starts with an environment. -Project APIs return HTTP 503 until the deployment enables `PROJECT_API_ENABLED`. Automatic assignment on legacy workspace creation requires `PROJECT_WRITES_ENABLED`; both server controls default off. The API requires writers to be enabled. Existing assigned Projects always retain their lifecycle protections, including fork inheritance and disconnect behavior, even if activation is disabled. +Project APIs return HTTP 503 until the deployment enables `PROJECT_API_ENABLED`. Workspace creation always assigns a Project atomically. The API control defaults off and does not disable assignment. Existing assigned Projects always retain their lifecycle protections, including fork inheritance and disconnect behavior, even if activation is disabled. ## Choose the creation flow | Caller | Flow | Result | | --- | --- | --- | | New Project onboarding | `POST /api/projects` | Creates a Project and its first environment together, with independently supplied names. | -| Existing workspace creation UI or caller | `POST /api/workspaces` | Creates a workspace and, when Project writers are enabled, automatically creates its Project, preserving the existing workspace response. | +| Existing workspace creation UI or caller | `POST /api/workspaces` | Creates a workspace and automatically creates its Project, preserving the existing workspace response. | | Create another environment by forking | Existing workspace fork operation | Inherits the source workspace's Project; unassigned legacy families remain unassigned until backfilled. | Both POST endpoints are internal, session-authenticated APIs. `POST /api/projects` is not a public `/api/v2` endpoint and does not accept API-key principals. This foundation does not remove or deprecate existing workspace creation endpoints. @@ -69,7 +69,7 @@ Existing callers can continue to use `createWorkspaceContract` and `POST /api/wo `skipDefaultWorkflow` remains optional and defaults to `false`. The route uses the session's active organization and existing workspace creation policy to resolve ownership and billing. It does not take the explicit Project scope or independent Project name used by `POST /api/projects`. -When Project writers are enabled, the workspace and its Project are created atomically. The generated Project name is `Support workspace - Project`; long names are bounded to 100 characters while retaining the suffix. The response remains `{ "workspace": ... }` with HTTP 200, without a new Project response wrapper. Call `GET /api/projects/by-workspace/[workspaceId]` when an existing workspace caller needs its authorized Project details. +The workspace and its Project are created atomically. The generated Project name is `Support workspace - Project`; long names are bounded to 100 characters while retaining the suffix. The response remains `{ "workspace": ... }` with HTTP 200, without a new Project response wrapper. Call `GET /api/projects/by-workspace/[workspaceId]` when an existing workspace caller needs its authorized Project details. ## Server implementation diff --git a/apps/sim/lib/projects/__integration__/foundation.integration.ts b/apps/sim/lib/projects/__integration__/foundation.integration.ts index 839f2190d5e..2512f49cb4d 100644 --- a/apps/sim/lib/projects/__integration__/foundation.integration.ts +++ b/apps/sim/lib/projects/__integration__/foundation.integration.ts @@ -55,7 +55,6 @@ import { createFork } from '@/ee/workspace-forking/lib/create-fork' import { unlinkForkEdge } from '@/ee/workspace-forking/lib/lineage/unlink' beforeEach(() => { - vi.stubEnv('PROJECT_WRITES_ENABLED', 'true') vi.stubEnv('PROJECT_API_ENABLED', 'true') }) @@ -194,9 +193,8 @@ afterAll(async () => { describe('Project foundation at the database and application boundary', () => { check( - 'disabled rollout preserves legacy creation and fork/disconnect without Project rows', + 'workspace creation and fork/disconnect assign Projects while APIs remain disabled', async () => { - vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') vi.stubEnv('PROJECT_API_ENABLED', 'false') const f = await fixture(false, 1) const source = await db.transaction((tx) => @@ -214,7 +212,7 @@ describe('Project foundation at the database and application boundary', () => { environments.push(source.id) expect( await db.select().from(projectWorkspace).where(eq(projectWorkspace.workspaceId, source.id)) - ).toEqual([]) + ).toHaveLength(1) const parent = await getWorkspaceWithOwner(source.id) if (!parent) throw new Error('Missing source fixture') const fork = await createFork({ @@ -229,11 +227,11 @@ describe('Project foundation at the database and application boundary', () => { .select() .from(projectWorkspace) .where(eq(projectWorkspace.workspaceId, fork.workspace.id)) - ).toEqual([]) + ).toHaveLength(1) await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) const [child] = await db.select().from(workspace).where(eq(workspace.id, fork.workspace.id)) expect(child.forkedFromWorkspaceId).toBeNull() - expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(1) + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(3) } ) @@ -242,45 +240,42 @@ describe('Project foundation at the database and application boundary', () => { async () => { const f = await fixture(false, 1) vi.stubEnv('PROJECT_API_ENABLED', 'false') - for (const writes of ['false', 'true']) { - vi.stubEnv('PROJECT_WRITES_ENABLED', writes) - const input = { projectId: f.projectId } - const calls = [ - () => - createProject.execute({ - principal: f.owner, - input: { - organizationId: null, - name: 'Blocked', - initialEnvironment: { name: 'Production' }, - }, - request, - }), - () => getProject.execute({ principal: f.owner, input, request }), - () => - getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: f.ids[0] }, - request, - }), - () => listProjects.execute({ principal: f.owner, input: { limit: 10 }, request }), - () => - renameProject.execute({ - principal: f.owner, - input: { ...input, name: 'Blocked' }, - request, - }), - () => archiveProject.execute({ principal: f.owner, input, request }), - () => getProjectIssueAccess.execute({ principal: f.owner, input, request }), - ] - for (const call of calls) await expect(call()).rejects.toMatchObject({ statusCode: 503 }) - expect( - await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) - ).toHaveLength(1) - const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) - expect(record.name).toBe('Environment 0 - Project') - expect(record.archivedAt).toBeNull() - } + const input = { projectId: f.projectId } + const calls = [ + () => + createProject.execute({ + principal: f.owner, + input: { + organizationId: null, + name: 'Blocked', + initialEnvironment: { name: 'Production' }, + }, + request, + }), + () => getProject.execute({ principal: f.owner, input, request }), + () => + getWorkspaceProject.execute({ + principal: f.owner, + input: { workspaceId: f.ids[0] }, + request, + }), + () => listProjects.execute({ principal: f.owner, input: { limit: 10 }, request }), + () => + renameProject.execute({ + principal: f.owner, + input: { ...input, name: 'Blocked' }, + request, + }), + () => archiveProject.execute({ principal: f.owner, input, request }), + () => getProjectIssueAccess.execute({ principal: f.owner, input, request }), + ] + for (const call of calls) await expect(call()).rejects.toMatchObject({ statusCode: 503 }) + expect( + await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId)) + ).toHaveLength(1) + const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) + expect(record.name).toBe('Environment 0 - Project') + expect(record.archivedAt).toBeNull() } ) @@ -288,7 +283,6 @@ describe('Project foundation at the database and application boundary', () => { 'disabling activation preserves assigned fork membership and lifecycle protections', async () => { const f = await fixture(false, 1) - vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') vi.stubEnv('PROJECT_API_ENABLED', 'false') const parent = await getWorkspaceWithOwner(f.ids[0]) if (!parent) throw new Error('Missing source fixture') @@ -316,7 +310,7 @@ describe('Project foundation at the database and application boundary', () => { } ) - check('writer activation assigns new workspaces while Project APIs remain disabled', async () => { + check('new workspaces receive Projects while Project APIs remain disabled', async () => { vi.stubEnv('PROJECT_API_ENABLED', 'false') const f = await fixture(false, 1) const created = await db.transaction((tx) => @@ -335,18 +329,6 @@ describe('Project foundation at the database and application boundary', () => { expect( await db.select().from(projectWorkspace).where(eq(projectWorkspace.workspaceId, created.id)) ).toHaveLength(1) - vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') - vi.stubEnv('PROJECT_API_ENABLED', 'true') - await expect( - createProject.execute({ - principal: f.owner, - input: { organizationId: null, name: 'Invalid', initialEnvironment: { name: 'First' } }, - request, - }) - ).rejects.toThrow('PROJECT_API_ENABLED requires PROJECT_WRITES_ENABLED') - expect(await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId))).toHaveLength( - 2 - ) }) check('legacy fork and disconnect refuse a partially assigned subtree', async () => { @@ -354,7 +336,6 @@ describe('Project foundation at the database and application boundary', () => { await db .delete(projectWorkspace) .where(inArray(projectWorkspace.workspaceId, f.ids.slice(0, 2))) - vi.stubEnv('PROJECT_WRITES_ENABLED', 'false') vi.stubEnv('PROJECT_API_ENABLED', 'false') const parent = await getWorkspaceWithOwner(f.ids[1]) if (!parent) throw new Error('Missing source fixture') @@ -395,20 +376,23 @@ describe('Project foundation at the database and application boundary', () => { }) try { await Promise.race([read.promise, writer]) - await expect( - db.transaction(async (tx) => { - await tx.execute( - sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` - ) - }) - ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '55P03') + await db.transaction(async (tx) => { + const [lock] = await tx.execute<{ acquired: boolean }>(sql` + SELECT pg_try_advisory_xact_lock(hashtextextended(${`project-backfill:${f.ids[0]}`}, 0)) AS acquired + `) + expect(lock.acquired).toBe(false) + const [unrelated] = await tx.execute<{ acquired: boolean }>(sql` + SELECT pg_try_advisory_xact_lock(hashtextextended('project-backfill:unrelated', 0)) AS acquired + `) + expect(unrelated.acquired).toBe(true) + }) } finally { release.resolve() await writer } await db.transaction(async (tx) => { await tx.execute( - sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`project-backfill:${f.ids[0]}`}, 0))` ) await createProjectForWorkspace(tx, { workspaceId: f.ids[0], @@ -434,7 +418,7 @@ describe('Project foundation at the database and application boundary', () => { const release = createDeferred() const backfill = db.transaction(async (tx) => { await tx.execute( - sql`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`project-backfill:${f.ids[0]}`}, 0))` ) locked.resolve() await release.promise @@ -459,7 +443,7 @@ describe('Project foundation at the database and application boundary', () => { try { for (let attempt = 0; attempt < 100; attempt++) { const rows = await db.execute<{ waiting: boolean }>(sql`SELECT EXISTS ( - SELECT 1 FROM pg_locks WHERE relation = 'workspace'::regclass AND mode = 'RowExclusiveLock' AND NOT granted + SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND mode = 'ShareLock' AND NOT granted ) AS waiting`) if (rows[0]?.waiting) { blocked = true diff --git a/apps/sim/lib/projects/account-deletion.ts b/apps/sim/lib/projects/account-deletion.ts index 795c999dd57..98e14251a87 100644 --- a/apps/sim/lib/projects/account-deletion.ts +++ b/apps/sim/lib/projects/account-deletion.ts @@ -10,7 +10,14 @@ export async function prepareProjectsForAccountDeletion( userId: string, doomedWorkspaceIds: string[] ): Promise { - await lockProjectBackfillWrites(tx) + const ownedEnvironments = await tx + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.ownerId, userId)) + await lockProjectBackfillWrites(tx, [ + ...doomedWorkspaceIds, + ...ownedEnvironments.map((row) => row.id), + ]) const records = await tx .select({ id: project.id }) .from(project) diff --git a/apps/sim/lib/projects/membership.ts b/apps/sim/lib/projects/membership.ts index 54c45c6e36c..5efe1592ecb 100644 --- a/apps/sim/lib/projects/membership.ts +++ b/apps/sim/lib/projects/membership.ts @@ -5,11 +5,21 @@ import { and, asc, eq, inArray, isNull, ne, notInArray, sql } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx, DbTransaction } from '@/lib/db/types' -/** Prevents backfill from assigning membership between an absence read and the ensuing write. */ -export async function lockProjectBackfillWrites(tx: DbTransaction): Promise { +/** Shared per-environment gate keeps membership absence reads stable during SQL backfill. */ +export async function lockProjectBackfillWrites( + tx: DbTransaction, + workspaceIds: string[] +): Promise { + if (!workspaceIds.length) return await tx.execute(sql`SET LOCAL lock_timeout = '5s'`) try { - await tx.execute(sql`LOCK TABLE workspace IN ROW EXCLUSIVE MODE`) + await tx.execute(sql` + SELECT pg_advisory_xact_lock_shared(hashtextextended('project-backfill:' || id, 0)) + FROM (SELECT DISTINCT unnest(ARRAY[${sql.join( + workspaceIds.map((id) => sql`${id}`), + sql`, ` + )}]::text[]) AS id ORDER BY id) ids + `) } catch (error) { if (getPostgresErrorCode(error) === '55P03') throw new OrchestrationError('conflict', 'Project backfill is running; retry the operation') @@ -59,9 +69,9 @@ export async function createProjectForWorkspace( return id } -/** Returns null only for a legacy workspace awaiting the operator-run backfill. */ +/** Returns null only for a legacy workspace awaiting the SQL backfill. */ export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: string) { - await lockProjectBackfillWrites(tx) + await lockProjectBackfillWrites(tx, [workspaceId]) const [membership] = await tx .select() .from(projectWorkspace) @@ -218,7 +228,7 @@ export async function transferWorkspaceProjects( ownerId?: string ): Promise { if (!workspaceIds.length) return - await lockProjectBackfillWrites(tx) + await lockProjectBackfillWrites(tx, workspaceIds) const owners = await tx .selectDistinct({ id: projectWorkspace.projectId }) .from(projectWorkspace) diff --git a/apps/sim/lib/projects/rollout.server.ts b/apps/sim/lib/projects/rollout.server.ts index 3ffe5d81e8c..1f6e24962e4 100644 --- a/apps/sim/lib/projects/rollout.server.ts +++ b/apps/sim/lib/projects/rollout.server.ts @@ -1,15 +1,6 @@ import { envBoolean, getEnv } from '@/lib/core/config/env' import { HttpError } from '@/lib/core/utils/http-error' -/** Deployment-wide controls; existing Project lifecycle maintenance never depends on these. */ -export function getProjectRollout() { - const writesEnabled = envBoolean(getEnv('PROJECT_WRITES_ENABLED')) ?? false - const apiEnabled = envBoolean(getEnv('PROJECT_API_ENABLED')) ?? false - if (apiEnabled && !writesEnabled) - throw new Error('PROJECT_API_ENABLED requires PROJECT_WRITES_ENABLED') - return { writesEnabled, apiEnabled } -} - class ProjectUnavailableError extends HttpError { readonly statusCode = 503 constructor() { @@ -18,5 +9,5 @@ class ProjectUnavailableError extends HttpError { } export function requireProjectApiEnabled(): void { - if (!getProjectRollout().apiEnabled) throw new ProjectUnavailableError() + if (!(envBoolean(getEnv('PROJECT_API_ENABLED')) ?? false)) throw new ProjectUnavailableError() } diff --git a/apps/sim/lib/workspaces/create.ts b/apps/sim/lib/workspaces/create.ts index d81847a487f..4b1549bd0ae 100644 --- a/apps/sim/lib/workspaces/create.ts +++ b/apps/sim/lib/workspaces/create.ts @@ -6,7 +6,7 @@ import { generateId } from '@sim/utils/id' import { PlatformEvents } from '@/lib/core/telemetry' import type { DbTransaction } from '@/lib/db/types' import { createProjectForWorkspace } from '@/lib/projects/membership' -import { getProjectRollout, requireProjectApiEnabled } from '@/lib/projects/rollout.server' +import { requireProjectApiEnabled } from '@/lib/projects/rollout.server' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -94,7 +94,6 @@ async function createWorkspaceRecordsInTransaction( tx: DbTransaction, { projectName, - assignProject, userId, observedOrganizationId, name, @@ -103,8 +102,8 @@ async function createWorkspaceRecordsInTransaction( workspaceMode, billedAccountUserId, governingPermissionGroupOrganizationId, - }: TransactionalCreateWorkspaceParams & { projectName?: string; assignProject: boolean } -): Promise<{ projectId: string | null; workspace: CreatedWorkspace }> { + }: TransactionalCreateWorkspaceParams & { projectName?: string } +): Promise<{ projectId: string; workspace: CreatedWorkspace }> { const workspaceId = generateId() const workflowId = generateId() const now = new Date() @@ -133,15 +132,13 @@ async function createWorkspaceRecordsInTransaction( updatedAt: now, }) - const projectId = assignProject - ? await createProjectForWorkspace(tx, { - projectName, - workspaceId, - name, - organizationId: organizationId ?? null, - ownerId: userId, - }) - : null + const projectId = await createProjectForWorkspace(tx, { + projectName, + workspaceId, + name, + organizationId: organizationId ?? null, + ownerId: userId, + }) const permissionRows = [ { @@ -213,9 +210,7 @@ export async function createWorkspaceWithProjectInTransaction( params: TransactionalCreateWorkspaceParams & { projectName?: string } ): Promise<{ projectId: string; workspace: CreatedWorkspace }> { requireProjectApiEnabled() - const created = await createWorkspaceRecordsInTransaction(tx, { ...params, assignProject: true }) - if (!created.projectId) throw new Error('Project creation did not assign a Project') - return { ...created, projectId: created.projectId } + return createWorkspaceRecordsInTransaction(tx, params) } /** Preserves the workspace-only result for existing creation callers. */ @@ -223,12 +218,7 @@ export async function createWorkspaceInTransaction( tx: DbTransaction, params: TransactionalCreateWorkspaceParams ): Promise { - return ( - await createWorkspaceRecordsInTransaction(tx, { - ...params, - assignProject: getProjectRollout().writesEnabled, - }) - ).workspace + return (await createWorkspaceRecordsInTransaction(tx, params)).workspace } /** Creates a workspace through the canonical lock-and-insert transaction. */ diff --git a/packages/db/package.json b/packages/db/package.json index 9ff079b5d8c..31cf5145650 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -28,10 +28,6 @@ "./sso-primary-provider": { "types": "./sso-primary-provider.ts", "default": "./sso-primary-provider.ts" - }, - "./project-backfill": { - "types": "./project-backfill.ts", - "default": "./project-backfill.ts" } }, "scripts": { diff --git a/packages/db/project-backfill.ts b/packages/db/project-backfill.ts deleted file mode 100644 index 181cfe17246..00000000000 --- a/packages/db/project-backfill.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { createHash } from 'node:crypto' -import { createLogger } from '@sim/logger' -import { classifyDatabaseFailure, getPostgresErrorCode } from '@sim/utils/errors' -import { sleep } from '@sim/utils/helpers' -import { generateId } from '@sim/utils/id' -import { backoffWithJitter } from '@sim/utils/retry' -import { compareStrings, truncate } from '@sim/utils/string' -import postgres, { type Sql, type TransactionSql } from 'postgres' - -type BackfillSql = Sql | TransactionSql - -const logger = createLogger('ProjectBackfill', { enabled: true, logLevel: 'INFO' }) -const MAX_FAMILY = 10_000 -const MAX_WORKSPACES = 250_000 -const RUN_LOCK = 'sim-project-backfill-v1' - -interface LegacyWorkspace { - id: string - name: string - organization_id: string | null - owner_id: string - archived_at: string | null - forked_from_workspace_id: string | null -} - -interface FamilyPlan { - rootId: string - projectId: string - workspaceIds: string[] - fingerprint: string - name: string -} - -interface Conflict { - workspaceId: string - projectIds: string[] - workspaceIds: string[] - reason: string -} - -export interface ProjectBackfillReport { - version: 1 - databaseId: string - mode: 'dry-run' | 'apply' | 'verify' - startedAt: string - finishedAt?: string - completed: boolean - ready: boolean - stopped?: string - errorCode?: string - scanned: number - families: number - createdProjects: number - assignedWorkspaces: number - unchangedFamilies: number - retries: number - plan: FamilyPlan[] - conflicts: Conflict[] -} - -interface BackfillOptions { - mode: ProjectBackfillReport['mode'] - databaseId: string - plan?: ProjectBackfillReport - maxFamilySize?: number - seconds?: number - shouldStop?: () => boolean - onProgress?: (report: ProjectBackfillReport) => Promise -} - -function fingerprint(family: LegacyWorkspace[]): string { - return createHash('sha256').update(JSON.stringify(family)).digest('hex') -} - -function conflict( - rootId: string, - reason: string, - ids: string[], - projects: string[] = [] -): Conflict { - return { workspaceId: rootId, reason, workspaceIds: ids, projectIds: projects } -} - -/** Only reads and transactions known to have rolled back may be retried. */ -async function withDatabaseRetry( - operation: () => Promise, - write: boolean, - report: ProjectBackfillReport, - stopped: () => boolean -): Promise { - for (let attempt = 1; ; attempt++) { - try { - return await operation() - } catch (error) { - const code = getPostgresErrorCode(error) - const category = classifyDatabaseFailure(error) - const retryable = write - ? ['40001', '40P01', '55P03'].includes(code ?? '') - : category === 'connection' || ['53300', '57P03'].includes(code ?? '') - if (!retryable || attempt >= 3 || stopped()) throw error - report.retries++ - logger.warn('Retrying Project backfill database operation', { code, write, attempt }) - await sleep(backoffWithJitter(attempt, null, { baseMs: 250, maxMs: 2000 })) - } - } -} - -async function configureTransaction(tx: BackfillSql) { - await tx`SET LOCAL lock_timeout = '2s'` - await tx`SET LOCAL statement_timeout = '30s'` - await tx`SET LOCAL idle_in_transaction_session_timeout = '30s'` - await tx`SET LOCAL transaction_timeout = '30s'` -} - -async function discoverFamily(tx: BackfillSql, rootId: string, maximum: number) { - const family = await tx` - WITH RECURSIVE family AS ( - SELECT id, name, organization_id, owner_id, archived_at::text AS archived_at, forked_from_workspace_id FROM workspace WHERE id = ${rootId} - UNION - SELECT w.id, w.name, w.organization_id, w.owner_id, w.archived_at::text, w.forked_from_workspace_id - FROM workspace w JOIN family f ON w.forked_from_workspace_id = f.id - ) SELECT * FROM family LIMIT ${maximum + 1} - ` - return family.sort((a, b) => compareStrings(a.id, b.id)) -} - -async function inspectFamily(tx: BackfillSql, rootId: string, maximum: number) { - const family = await discoverFamily(tx, rootId, maximum) - const root = family.find((row) => row.id === rootId) - const ids = family.map((row) => row.id) - if (!root || root.forked_from_workspace_id !== null) - return { conflict: conflict(rootId, 'Root missing or now attached to another family', ids) } - if (family.length > maximum) - return { conflict: conflict(rootId, 'Family exceeds batch limit', ids) } - if (family.some((row) => row.organization_id !== root.organization_id)) - return { conflict: conflict(rootId, 'Family spans organizations', ids) } - const memberships = await tx<{ project_id: string }[]>` - SELECT DISTINCT project_id FROM project_workspace WHERE workspace_id = ANY(${ids}::text[]) ORDER BY project_id - ` - const projectIds = memberships.map((row) => row.project_id) - if (projectIds.length > 1) - return { conflict: conflict(rootId, 'Family spans multiple Projects', ids, projectIds) } - const existingId = projectIds[0] - if (existingId) { - const [existing] = await tx<{ organization_id: string | null; archived_at: Date | null }[]>` - SELECT organization_id, archived_at FROM project WHERE id = ${existingId} - ` - if (!existing || existing.organization_id !== root.organization_id) - return { conflict: conflict(rootId, 'Project organization differs', ids, projectIds) } - const outside = - await tx`SELECT workspace_id FROM project_workspace WHERE project_id = ${existingId} AND NOT (workspace_id = ANY(${ids}::text[])) LIMIT 1` - if (outside.length) - return { conflict: conflict(rootId, 'Project includes another lineage', ids, projectIds) } - if (Boolean(existing.archived_at) !== family.every((row) => row.archived_at !== null)) - return { conflict: conflict(rootId, 'Project archive state differs', ids, projectIds) } - } - const [{ missing }] = await tx<{ missing: number }[]>` - SELECT count(*)::int AS missing FROM workspace w WHERE w.id = ANY(${ids}::text[]) - AND NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.workspace_id = w.id) - ` - return { root, family, ids, existingId, missing } -} - -async function applyFamily(tx: BackfillSql, planned: FamilyPlan, maximum: number) { - // NOWAIT prevents waiting behind application writers with an opposite row/advisory lock order. - await tx`LOCK TABLE workspace, project, project_workspace IN SHARE ROW EXCLUSIVE MODE NOWAIT` - const state = await inspectFamily(tx, planned.rootId, maximum) - if (state.conflict) return { conflict: state.conflict, created: 0, assigned: 0 } - if (fingerprint(state.family) !== planned.fingerprint) - return { - conflict: conflict( - planned.rootId, - 'Family changed since dry-run; produce a new plan', - state.ids - ), - created: 0, - assigned: 0, - } - if (state.existingId && state.existingId !== planned.projectId) - return { - conflict: conflict(planned.rootId, 'Project assignment changed since dry-run', state.ids, [ - state.existingId, - planned.projectId, - ]), - created: 0, - assigned: 0, - } - if (!state.existingId) { - await tx`INSERT INTO project (id, name, organization_id, owner_id, archived_at) - SELECT ${planned.projectId}, ${planned.name}, ${state.root.organization_id}, ${state.root.owner_id}, - CASE WHEN count(archived_at) = count(*) THEN max(archived_at) ELSE NULL END - FROM workspace WHERE id = ANY(${state.ids}::text[])` - } - const inserted = await tx`INSERT INTO project_workspace (project_id, workspace_id) - SELECT ${planned.projectId}, id FROM workspace WHERE id = ANY(${state.ids}::text[]) - AND NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.workspace_id = workspace.id)` - return { created: state.existingId ? 0 : 1, assigned: inserted.count } -} - -async function inspectGlobalState(tx: BackfillSql) { - const rows = await tx<{ id: string; reason: string }[]>` - SELECT p.id, 'Empty Project' AS reason FROM project p - WHERE NOT EXISTS (SELECT 1 FROM project_workspace pw WHERE pw.project_id = p.id) - UNION ALL - SELECT pw.project_id, 'Orphan membership or organization mismatch' FROM project_workspace pw - LEFT JOIN project p ON p.id = pw.project_id LEFT JOIN workspace w ON w.id = pw.workspace_id - WHERE p.id IS NULL OR w.id IS NULL OR p.organization_id IS DISTINCT FROM w.organization_id - LIMIT 1001 - ` - if (rows.length > 1000) throw new Error('Global anomaly report limit exceeded; repair and rerun') - return rows.map((row) => conflict('', row.reason, [], [row.id])) -} - -/** Validate the operator artifact before it can select any write target. */ -export function readProjectBackfillPlan(value: unknown, databaseId: string): ProjectBackfillReport { - if (!value || typeof value !== 'object') throw new Error('Invalid dry-run artifact') - const plan = value as Partial - if ( - plan.version !== 1 || - plan.mode !== 'dry-run' || - !plan.completed || - plan.databaseId !== databaseId || - !Array.isArray(plan.plan) || - !Array.isArray(plan.conflicts) || - plan.conflicts.length - ) - throw new Error('Apply requires a completed, conflict-free dry-run for this database') - let total = 0 - const roots = new Set() - const projects = new Set() - const members = new Set() - for (const family of plan.plan) { - if ( - !family || - typeof family.rootId !== 'string' || - !family.rootId || - typeof family.projectId !== 'string' || - !family.projectId || - typeof family.fingerprint !== 'string' || - !/^[a-f0-9]{64}$/.test(family.fingerprint) || - typeof family.name !== 'string' || - family.name.trim().length < 1 || - family.name.length > 100 || - !Array.isArray(family.workspaceIds) || - !family.workspaceIds.length || - family.workspaceIds.length > MAX_FAMILY - ) - throw new Error('Malformed family in dry-run artifact') - if (roots.has(family.rootId) || projects.has(family.projectId)) - throw new Error('Duplicate planned root or Project') - roots.add(family.rootId) - projects.add(family.projectId) - for (const id of family.workspaceIds) { - if (typeof id !== 'string' || !id || members.has(id)) - throw new Error('Invalid or repeated planned environment') - members.add(id) - } - if (!family.workspaceIds.includes(family.rootId)) - throw new Error('Root missing from dry-run family') - total += family.workspaceIds.length - if (total > MAX_WORKSPACES) throw new Error('Dry-run artifact exceeds workspace limit') - } - return plan as ProjectBackfillReport -} - -/** Bounded operator backfill. Reports are checkpointed before writes and after each commit. */ -export async function backfillProjects( - sql: Sql, - options: BackfillOptions -): Promise { - const maximum = options.maxFamilySize ?? MAX_FAMILY - const seconds = options.seconds ?? 600 - if ( - !Number.isInteger(maximum) || - maximum < 1 || - maximum > MAX_FAMILY || - !Number.isInteger(seconds) || - seconds < 1 || - seconds > 3600 - ) - throw new Error('Invalid family or runtime limit') - const source = - options.mode === 'apply' ? readProjectBackfillPlan(options.plan, options.databaseId) : null - const report: ProjectBackfillReport = { - version: 1, - databaseId: options.databaseId, - mode: options.mode, - startedAt: new Date().toISOString(), - completed: false, - ready: false, - scanned: 0, - families: 0, - createdProjects: 0, - assignedWorkspaces: 0, - unchangedFamilies: 0, - retries: 0, - plan: [], - conflicts: [], - } - const deadline = Date.now() + seconds * 1000 - const stopped = () => Boolean(options.shouldStop?.()) || Date.now() >= deadline - const checkpoint = async () => { - await options.onProgress?.(report) - } - let lockLost = false - if (sql.options.host.length !== 1 || sql.options.port.length !== 1) - throw new Error('Backfill requires a single direct primary endpoint') - const lockClient = postgres({ - host: sql.options.host[0], - port: sql.options.port[0], - database: sql.options.database, - user: sql.options.user, - password: sql.options.pass ?? undefined, - ssl: sql.options.ssl, - connection: sql.options.connection, - onnotice: sql.options.onnotice, - max: 1, - idle_timeout: 0, - max_lifetime: null, - connect_timeout: 10, - onclose: () => { - lockLost = true - }, - }) - const connection = await lockClient.reserve() - let locked = false - let backendPid = 0 - try { - // Session settings also bound discovery and the first lock attempt. - await connection`SET statement_timeout = '30s'` - await connection`SET lock_timeout = '2s'` - const [identity] = await connection<{ pid: number; locked: boolean }[]>` - SELECT pg_backend_pid() AS pid, pg_try_advisory_lock(hashtextextended(${RUN_LOCK}, 0)) AS locked - ` - locked = identity.locked - backendPid = identity.pid - if (!locked) throw new Error('Another Project backfill run holds the maintenance lock') - const assertLock = async () => { - if (lockLost) throw new Error('Project backfill maintenance lock was lost') - const [state] = await connection<{ held: boolean }[]>` - SELECT pg_backend_pid() = ${backendPid} AND EXISTS ( - SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid() - AND classid = ((hashtextextended(${RUN_LOCK}, 0) >> 32) & 4294967295)::oid - AND objid = (hashtextextended(${RUN_LOCK}, 0) & 4294967295)::oid AND objsubid = 1 AND granted - ) AS held - ` - if (!state.held) throw new Error('Project backfill maintenance lock was lost') - } - const transaction = async ( - work: (tx: BackfillSql) => Promise, - write = false - ): Promise => - withDatabaseRetry( - async () => { - await assertLock() - const result = await sql.begin(async (tx) => { - await configureTransaction(tx) - const value = await work(tx) - await assertLock() - return value - }) - return result as T - }, - write, - report, - stopped - ) - await checkpoint() - if (source) { - for (const family of source.plan) { - if (stopped()) break - const outcome = await transaction((tx) => applyFamily(tx, family, maximum), true) - report.scanned += family.workspaceIds.length - report.families++ - if (outcome.conflict) report.conflicts.push(outcome.conflict) - report.createdProjects += outcome.created - report.assignedWorkspaces += outcome.assigned - if (!outcome.conflict && outcome.assigned === 0) report.unchangedFamilies++ - await checkpoint() - } - report.completed = report.families === source.plan.length - } else { - let afterId = '' - for (;;) { - if (stopped()) break - const page = await transaction( - (tx) => - tx< - { id: string }[] - >`SELECT id FROM workspace WHERE id > ${afterId} ORDER BY id LIMIT 100` - ) - if (!page.length) { - report.completed = true - break - } - for (const candidate of page) { - if (stopped()) break - if (++report.scanned > MAX_WORKSPACES) throw new Error('Workspace scan limit exceeded') - const discovery = await transaction(async (tx) => { - const [root] = await tx<{ id: string; parent: string | null; cycle: boolean }[]>` - WITH RECURSIVE ancestors AS ( - SELECT id, forked_from_workspace_id AS parent, ARRAY[id] AS path, false AS cycle, 0 AS depth FROM workspace WHERE id = ${candidate.id} - UNION ALL - SELECT w.id, w.forked_from_workspace_id, a.path || w.id, w.id = ANY(a.path), a.depth + 1 - FROM workspace w JOIN ancestors a ON w.id = a.parent WHERE NOT a.cycle AND a.depth < 1000 - ) SELECT id, parent, cycle FROM ancestors ORDER BY depth DESC LIMIT 1 - ` - if (!root || root.cycle || root.parent !== null) - return { - conflict: conflict( - candidate.id, - 'Cycle, missing parent, or lineage depth exceeds 1000', - [candidate.id] - ), - } - if (root.id !== candidate.id) return null - return inspectFamily(tx, root.id, maximum) - }) - if (discovery) { - if (discovery.conflict) report.conflicts.push(discovery.conflict) - else { - report.families++ - const suffix = ' - Project' - report.plan.push({ - rootId: candidate.id, - projectId: discovery.existingId ?? generateId(), - workspaceIds: discovery.ids, - fingerprint: fingerprint(discovery.family), - name: `${truncate(discovery.root.name.trim() || 'Untitled', 100 - suffix.length, '')}${suffix}`, - }) - report.createdProjects += discovery.existingId ? 0 : 1 - report.assignedWorkspaces += discovery.missing - if (discovery.missing === 0) report.unchangedFamilies++ - } - } - afterId = candidate.id - } - await checkpoint() - } - if (report.completed) report.conflicts.push(...(await transaction(inspectGlobalState))) - } - report.ready = - options.mode === 'verify' && - report.completed && - !report.conflicts.length && - report.assignedWorkspaces === 0 - if (!report.completed) - report.stopped = 'Interrupted or runtime budget exhausted; rerun from the same plan' - report.finishedAt = new Date().toISOString() - await checkpoint() - return report - } catch (error) { - report.completed = false - report.ready = false - report.errorCode = getPostgresErrorCode(error) - report.stopped = report.errorCode - ? `Database operation failed (${report.errorCode}); reconcile before retrying writes` - : 'Run failed; inspect operator logs and last checkpoint' - report.finishedAt = new Date().toISOString() - await checkpoint() - throw error - } finally { - try { - if (locked && !lockLost) - await connection`SELECT pg_advisory_unlock(hashtextextended(${RUN_LOCK}, 0))` - } finally { - connection.release() - await lockClient.end({ timeout: 2 }) - } - } -} diff --git a/packages/db/scripts/backfill-projects.ts b/packages/db/scripts/backfill-projects.ts deleted file mode 100644 index 3dc9775b5e2..00000000000 --- a/packages/db/scripts/backfill-projects.ts +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env bun - -import { createHash } from 'node:crypto' -import { readFile, rename, stat, writeFile } from 'node:fs/promises' -import { resolve } from 'node:path' -import { parseArgs } from 'node:util' -import { backfillProjects, readProjectBackfillPlan } from '@sim/db/project-backfill' -import { createLogger } from '@sim/logger' -import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' -import postgres from 'postgres' - -const logger = createLogger('ProjectBackfillCommand', { enabled: true, logLevel: 'INFO' }) -const HELP = `Operator-only Project backfill. Deployment only creates the additive tables. - -bun --no-env-file packages/db/scripts/backfill-projects.ts [options] - -Commands: - identity Print the non-secret database fingerprint; no database writes. - dry-run Discover and validate families; write the plan/report. - apply Apply --from-file PLAN from a completed, conflict-free dry-run. - verify Independently reconcile all environments and Projects; nonzero unless ready. - -Required environment: PROJECT_BACKFILL_DATABASE_URL (direct primary connection). -Reports: PROJECT_BACKFILL_REPORT_PATH (required except identity). -Apply requires --writers-drained --project-writers-enabled --release-revision REVISION -and --database-id FINGERPRINT. These are operator assertions, not fleet verification. -Deploy first with PROJECT_WRITES_ENABLED=false and PROJECT_API_ENABLED=false. -Drain old code, enable Project writers on every compatible instance, then backfill. -Keep the Project API off until verification and later contract enforcement. -Once Project data exists, never roll back to code that predates Project support. -Options: --from-file PATH, --seconds 600 (1–3600), --max-family-size 1000 (1–10000). -Run with compatible writers deployed. Pause lineage/ownership writes during apply. -Short NOWAIT table locks exclude concurrent changes during each family transaction. -Interrupted/failed apply: inspect the report, then rerun the SAME dry-run artifact. -Connection loss during writes is never automatically retried; verify the committed state. -No run enables Project consumers or installs the deferred contract constraints.` - -async function main() { - const { positionals, values } = parseArgs({ - args: process.argv.slice(2), - allowPositionals: true, - options: { - help: { type: 'boolean' }, - 'from-file': { type: 'string' }, - 'writers-drained': { type: 'boolean' }, - 'project-writers-enabled': { type: 'boolean' }, - 'release-revision': { type: 'string' }, - 'database-id': { type: 'string' }, - seconds: { type: 'string', default: '600' }, - 'max-family-size': { type: 'string', default: '1000' }, - }, - }) - if (values.help) { - process.stdout.write(`${HELP}\n`) - return - } - const mode = positionals[0] ?? 'dry-run' - if (positionals.length > 1 || !['identity', 'dry-run', 'apply', 'verify'].includes(mode)) - throw new Error('Unknown command; use --help') - const rawUrl = process.env.PROJECT_BACKFILL_DATABASE_URL - if (!rawUrl) - throw new Error('Set PROJECT_BACKFILL_DATABASE_URL explicitly; no application DSN fallback') - const url = new URL(rawUrl) - if ( - !['postgres:', 'postgresql:'].includes(url.protocol) || - !url.hostname || - !url.username || - url.pathname.length < 2 - ) - throw new Error('Expected a direct PostgreSQL database URL') - const databaseId = createHash('sha256') - .update( - JSON.stringify([ - url.hostname.toLowerCase(), - url.port || '5432', - decodeURIComponent(url.pathname), - ]) - ) - .digest('hex') - if (mode === 'identity') { - process.stdout.write(`${databaseId}\n`) - return - } - if (mode !== 'dry-run' && mode !== 'apply' && mode !== 'verify') - throw new Error('Invalid command') - const reportPath = process.env.PROJECT_BACKFILL_REPORT_PATH - if (!reportPath) throw new Error('Set PROJECT_BACKFILL_REPORT_PATH explicitly') - const releaseRevision = values['release-revision']?.trim() ?? '' - if ( - mode === 'apply' && - (!values['writers-drained'] || - !values['project-writers-enabled'] || - !releaseRevision || - values['database-id'] !== databaseId || - !values['from-file']) - ) - throw new Error( - 'Apply requires --writers-drained, --project-writers-enabled, --release-revision, matching --database-id, and --from-file' - ) - if ( - mode !== 'apply' && - (values['writers-drained'] || - values['project-writers-enabled'] || - values['release-revision'] || - values['from-file']) - ) - throw new Error('Apply-only options used with read-only mode') - const operatorAssertions = - mode === 'apply' - ? { - oldWritersDrained: true, - projectWritersEnabled: true, - releaseRevision, - } - : undefined - const inputPath = values['from-file'] - if (inputPath && resolve(inputPath) === resolve(reportPath)) - throw new Error('Keep the dry-run plan and apply report in separate files') - const plan = inputPath - ? await (async () => { - if ((await stat(inputPath)).size > 32 * 1024 * 1024) - throw new Error('Dry-run artifact exceeds 32 MiB') - return readProjectBackfillPlan(JSON.parse(await readFile(inputPath, 'utf8')), databaseId) - })() - : undefined - const sql = postgres(rawUrl, { - max: 1, - prepare: false, - connect_timeout: 10, - idle_timeout: 0, - max_lifetime: null, - connection: { - application_name: 'sim-project-backfill', - statement_timeout: 30_000, - lock_timeout: 2000, - }, - }) - let interrupted = false - const stop = () => { - interrupted = true - } - process.on('SIGINT', stop) - process.on('SIGTERM', stop) - try { - // Preflight the report destination before opening a transaction that can write. - const partialPath = `${reportPath}.${process.pid}.partial` - await writeFile( - partialPath, - JSON.stringify({ - databaseId, - mode, - operatorAssertions, - completed: false, - ready: false, - stopped: 'Starting', - }), - { mode: 0o600 } - ) - await rename(partialPath, reportPath) - const report = await backfillProjects(sql, { - mode, - databaseId, - plan, - seconds: Number(values.seconds), - maxFamilySize: Number(values['max-family-size']), - shouldStop: () => interrupted, - onProgress: async (progress) => { - await writeFile(partialPath, JSON.stringify({ ...progress, operatorAssertions }, null, 2), { - mode: 0o600, - }) - await rename(partialPath, reportPath) - logger.info('Project backfill progress', { - mode, - databaseId, - scanned: progress.scanned, - families: progress.families, - createdProjects: progress.createdProjects, - assignedWorkspaces: progress.assignedWorkspaces, - conflicts: progress.conflicts.length, - retries: progress.retries, - completed: progress.completed, - }) - }, - }) - if (!report.completed || report.conflicts.length || (mode === 'verify' && !report.ready)) - process.exitCode = 1 - } finally { - process.off('SIGINT', stop) - process.off('SIGTERM', stop) - await sql.end({ timeout: 5 }) - } -} - -try { - await main() -} catch (error) { - const code = getPostgresErrorCode(error) - // Database messages can contain connection details or row values; retain SQLSTATE, not the payload. - logger.error('Project backfill failed', { - error: code - ? `Database error ${code}; inspect the report and reconcile before retrying` - : getErrorMessage(error), - }) - process.exitCode = 1 -} diff --git a/packages/db/scripts/project-backfill.integration.ts b/packages/db/scripts/project-backfill.integration.ts deleted file mode 100644 index dc4a4c30e8c..00000000000 --- a/packages/db/scripts/project-backfill.integration.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { spawn } from 'node:child_process' -import { readFile } from 'node:fs/promises' -import { backfillProjects, type ProjectBackfillReport } from '@sim/db/project-backfill' -import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' -import { generateId } from '@sim/utils/id' -import postgres, { type Sql } from 'postgres' -import { describe, expect, it } from 'vitest' - -/** Isolated pre-Project fixture; applies the actual additive migration, never a schema mock. */ -async function withDatabase(run: (sql: Sql, url: string) => Promise) { - const admin = postgres(readTestDatabaseUrl(), { max: 1 }) - const database = `project_backfill_test_${generateId().replaceAll('-', '')}` - const url = new URL(readTestDatabaseUrl()) - url.pathname = `/${database}` - await admin.unsafe(`CREATE DATABASE "${database}"`) - const sql = postgres(url.toString(), { max: 1, onnotice: () => undefined }) - try { - await sql.unsafe(` - CREATE TABLE "user" (id text PRIMARY KEY); - CREATE TABLE organization (id text PRIMARY KEY); - CREATE TABLE workspace ( - id text PRIMARY KEY, name text NOT NULL, owner_id text NOT NULL, - organization_id text, archived_at timestamp, forked_from_workspace_id text - ); - INSERT INTO "user" VALUES ('owner'), ('other-owner'); - INSERT INTO organization VALUES ('org-a'), ('org-b'); - `) - await sql.unsafe( - await readFile(new URL('../migrations/0393_project_foundation.sql', import.meta.url), 'utf8') - ) - await run(sql, url.toString()) - } finally { - await sql.end({ timeout: 2 }) - await admin.unsafe(`DROP DATABASE "${database}" WITH (FORCE)`) - await admin.end() - } -} - -async function seed(sql: Sql) { - await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id, archived_at) VALUES - ('a-root', 'Production', 'owner', 'org-a', NULL, NULL), - ('b-child', 'Staging', 'other-owner', 'org-a', 'a-root', NULL), - ('c-grandchild', 'Dev', 'owner', 'org-a', 'b-child', NULL), - ('d-detached', 'Staging', 'owner', NULL, NULL, NULL), - ('e-detached-child', 'Dev', 'owner', NULL, 'd-detached', NULL), - ('f-archived', 'Archive', 'owner', NULL, NULL, '2025-01-01'), - ('g-archived-child', 'Archive child', 'owner', NULL, 'f-archived', '2025-02-01')` -} - -const databaseId = 'local-test-database' -const dryRun = (sql: Sql) => backfillProjects(sql, { mode: 'dry-run', databaseId }) -const verify = (sql: Sql) => backfillProjects(sql, { mode: 'verify', databaseId }) -const apply = (sql: Sql, plan: ProjectBackfillReport) => - backfillProjects(sql, { mode: 'apply', databaseId, plan }) - -describe('Project backfill operator lifecycle', () => { - it('plans roots and detached families, retains archive timestamps, and replays fixed IDs', async () => { - await withDatabase(async (sql) => { - await seed(sql) - const plan = await dryRun(sql) - expect(plan.createdProjects).toBe(3) - expect(plan.assignedWorkspaces).toBe(7) - expect(await sql`SELECT * FROM project`).toHaveLength(0) - expect((await verify(sql)).ready).toBe(false) - const first = await apply(sql, plan) - expect(first.assignedWorkspaces).toBe(7) - expect((await verify(sql)).ready).toBe(true) - const rows = await sql`SELECT id, name, owner_id, archived_at FROM project ORDER BY name` - expect(rows.map((row) => row.name)).toEqual([ - 'Archive - Project', - 'Production - Project', - 'Staging - Project', - ]) - expect( - ( - await sql`SELECT archived_at::text AS value FROM project WHERE name = 'Archive - Project'` - )[0].value - ).toBe('2025-02-01 00:00:00') - expect(rows[1].owner_id).toBe('owner') - const repeat = await apply(sql, plan) - expect(repeat.createdProjects).toBe(0) - expect(repeat.unchangedFamilies).toBe(3) - expect(await sql`SELECT id, name, owner_id, archived_at FROM project ORDER BY name`).toEqual( - rows - ) - }) - }) - - it('completes partial assignments but blocks split Projects, cycles, missing parents and mixed organizations', async () => { - await withDatabase(async (sql) => { - await seed(sql) - await sql`INSERT INTO project (id, name, owner_id, organization_id) VALUES ('existing', 'Keep this name', 'owner', 'org-a')` - await sql`INSERT INTO project_workspace VALUES ('existing', 'a-root', now())` - const plan = await dryRun(sql) - await apply(sql, plan) - expect( - await sql`SELECT workspace_id FROM project_workspace WHERE project_id = 'existing'` - ).toHaveLength(3) - expect((await sql`SELECT name FROM project WHERE id = 'existing'`)[0].name).toBe( - 'Keep this name' - ) - await sql`INSERT INTO project (id, name, owner_id, organization_id) VALUES ('split', 'Split', 'owner', 'org-a'), ('empty', 'Empty', 'owner', NULL)` - await sql`UPDATE project_workspace SET project_id = 'split' WHERE workspace_id = 'b-child'` - await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) VALUES ('cycle-a', 'A', 'owner', 'cycle-b'), ('cycle-b', 'B', 'owner', 'cycle-a'), ('orphan', 'Orphan', 'owner', 'missing')` - await sql`UPDATE workspace SET organization_id = 'org-b' WHERE id = 'e-detached-child'` - const broken = await verify(sql) - expect(broken.ready).toBe(false) - expect(broken.conflicts.map((row) => row.reason)).toEqual( - expect.arrayContaining([ - 'Family spans multiple Projects', - 'Family spans organizations', - 'Empty Project', - 'Cycle, missing parent, or lineage depth exceeds 1000', - ]) - ) - expect(broken.conflicts.find((row) => row.workspaceId === 'a-root')?.projectIds).toEqual([ - 'existing', - 'split', - ]) - await expect(apply(sql, { ...broken, mode: 'dry-run' })).rejects.toThrow('conflict-free') - }) - }) - - it('rejects a new descendant added after dry-run without assigning a partial family', async () => { - await withDatabase(async (sql) => { - await seed(sql) - const plan = await dryRun(sql) - await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id) VALUES ('late-child', 'Late', 'owner', 'org-a', 'b-child')` - const result = await apply(sql, plan) - expect( - result.conflicts.some( - (row) => row.workspaceId === 'a-root' && row.reason.includes('changed since dry-run') - ) - ).toBe(true) - expect( - await sql`SELECT * FROM project_workspace WHERE workspace_id IN ('a-root', 'b-child', 'c-grandchild', 'late-child')` - ).toHaveLength(0) - const fresh = await dryRun(sql) - await apply(sql, fresh) - expect((await verify(sql)).ready).toBe(true) - }) - }) - - it('resumes after a committed-family interruption and retains progress when reporting fails', async () => { - await withDatabase(async (sql) => { - await seed(sql) - const plan = await dryRun(sql) - let stop = false - const partial = await backfillProjects(sql, { - mode: 'apply', - databaseId, - plan, - shouldStop: () => stop, - onProgress: async (report) => { - if (report.families === 1) stop = true - }, - }) - expect(partial.completed).toBe(false) - expect(partial.createdProjects).toBe(1) - const firstId = (await sql`SELECT id FROM project`)[0].id - await expect( - backfillProjects(sql, { - mode: 'apply', - databaseId, - plan, - onProgress: async (report) => { - if (report.createdProjects > 0) throw new Error('Report disk unavailable') - }, - }) - ).rejects.toThrow('Report disk unavailable') - expect(await sql`SELECT id FROM project`).toHaveLength(2) - await apply(sql, plan) - expect(await sql`SELECT id FROM project WHERE id = ${firstId}`).toHaveLength(1) - expect((await verify(sql)).ready).toBe(true) - }) - }) - - it('refuses another runner while a run owns the lock and stops on a lost database session', async () => { - await withDatabase(async (sql, url) => { - await seed(sql) - const other = postgres(url, { max: 1 }) - try { - let checked = false - await backfillProjects(sql, { - mode: 'dry-run', - databaseId, - onProgress: async () => { - if (checked) return - checked = true - await expect(dryRun(other)).rejects.toThrow('maintenance lock') - }, - }) - const plan = await dryRun(sql) - let terminated = false - let snapshot: ProjectBackfillReport | undefined - await expect( - backfillProjects(sql, { - mode: 'apply', - databaseId, - plan, - onProgress: async (report) => { - snapshot = structuredClone(report) - if (terminated || report.families !== 1) return - terminated = true - await other`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()` - }, - }) - ).rejects.toThrow() - expect(snapshot?.completed).toBe(false) - expect(snapshot?.createdProjects).toBe(1) - await apply(other, plan) - expect((await verify(other)).ready).toBe(true) - } finally { - await other.end({ timeout: 2 }) - } - }) - }) - - it('fails boundedly behind a live writer, then succeeds after the writer finishes', async () => { - await withDatabase(async (sql, url) => { - await seed(sql) - const plan = await dryRun(sql) - const writer = postgres(url, { max: 1 }) - try { - await writer.begin(async (tx) => { - await tx`UPDATE workspace SET name = name WHERE id = 'a-root'` - let last: ProjectBackfillReport | undefined - await expect( - backfillProjects(sql, { - mode: 'apply', - databaseId, - plan, - onProgress: async (report) => { - last = structuredClone(report) - }, - }) - ).rejects.toMatchObject({ code: '55P03' }) - expect(last?.retries).toBe(2) - expect(last?.createdProjects).toBe(0) - }) - await apply(sql, plan) - expect((await verify(sql)).ready).toBe(true) - } finally { - await writer.end() - } - }) - }) - - it('retries rolled-back serialization failures but stops on cancellation without partial writes', async () => { - await withDatabase(async (sql) => { - await seed(sql) - const plan = await dryRun(sql) - await sql.unsafe(` - CREATE SEQUENCE attempts; - CREATE FUNCTION fail_twice() RETURNS trigger LANGUAGE plpgsql AS $$ - BEGIN - IF nextval('attempts') <= 2 THEN RAISE EXCEPTION 'injected serialization failure' USING ERRCODE = '40001'; END IF; - RETURN NEW; - END $$; - CREATE TRIGGER fail_twice BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION fail_twice(); - `) - const result = await apply(sql, plan) - expect(result.retries).toBe(2) - expect(result.createdProjects).toBe(3) - expect((await verify(sql)).ready).toBe(true) - await sql`DELETE FROM project_workspace` - await sql`DELETE FROM project` - await sql.unsafe(` - DROP TRIGGER fail_twice ON project; - CREATE FUNCTION cancel_write() RETURNS trigger LANGUAGE plpgsql AS $$ - BEGIN RAISE EXCEPTION 'injected cancellation' USING ERRCODE = '57014'; END $$; - CREATE TRIGGER cancel_write BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION cancel_write(); - `) - let report: ProjectBackfillReport | undefined - await expect( - backfillProjects(sql, { - mode: 'apply', - databaseId, - plan, - onProgress: async (progress) => { - report = structuredClone(progress) - }, - }) - ).rejects.toMatchObject({ code: '57014' }) - expect(report?.retries).toBe(0) - expect(report?.errorCode).toBe('57014') - expect(await sql`SELECT * FROM project_workspace`).toHaveLength(0) - expect(await sql`SELECT * FROM project`).toHaveLength(0) - }) - }) - - it('fails readiness for empty Projects and archived-state mismatch without mutating them', async () => { - await withDatabase(async (sql) => { - await seed(sql) - await apply(sql, await dryRun(sql)) - await sql`UPDATE project SET archived_at = now() WHERE name = 'Production - Project'` - await sql`INSERT INTO project (id, name, owner_id) VALUES ('empty', 'Empty', 'owner')` - const result = await verify(sql) - expect(result.ready).toBe(false) - expect(result.conflicts.map((row) => row.reason)).toEqual( - expect.arrayContaining(['Empty Project', 'Project archive state differs']) - ) - expect(await sql`SELECT id FROM project WHERE id = 'empty'`).toHaveLength(1) - }) - }) -}) - -/** CLI cases exercise the normal connection contract against an isolated database. */ -describe('Project backfill CLI', () => { - it('runs dry-run/apply/verify with durable reports and refuses a mismatched target', async () => { - const { mkdtemp, rm } = await import('node:fs/promises') - const { tmpdir } = await import('node:os') - const { join } = await import('node:path') - await withDatabase(async (sql, url) => { - await seed(sql) - const directory = await mkdtemp(join(tmpdir(), 'project-backfill-test-')) - const planPath = join(directory, 'plan.json') - const reportPath = join(directory, 'report.json') - const run = async (args: string[], report = reportPath) => { - const child = spawn('bun', ['--no-env-file', 'scripts/backfill-projects.ts', ...args], { - env: { - ...process.env, - PROJECT_BACKFILL_DATABASE_URL: url, - PROJECT_BACKFILL_REPORT_PATH: report, - }, - stdio: ['ignore', 'pipe', 'pipe'], - }) - let stdout = '' - let stderr = '' - child.stdout.setEncoding('utf8').on('data', (chunk: string) => { - stdout += chunk - }) - child.stderr.setEncoding('utf8').on('data', (chunk: string) => { - stderr += chunk - }) - const code = await new Promise((resolve, reject) => { - child.on('error', reject) - child.on('close', resolve) - }) - expect(stdout + stderr).not.toContain(url) - return { stdout, stderr, code } - } - try { - const dry = await run(['dry-run'], planPath) - expect(dry.code, dry.stdout + dry.stderr).toBe(0) - const plan = JSON.parse(await readFile(planPath, 'utf8')) as ProjectBackfillReport - for (const missing of [ - '--project-writers-enabled', - '--release-revision', - '--writers-drained', - ]) { - const args = ['apply', '--from-file', planPath, '--database-id', plan.databaseId] - if (missing !== '--writers-drained') args.push('--writers-drained') - if (missing !== '--project-writers-enabled') args.push('--project-writers-enabled') - if (missing !== '--release-revision') - args.push('--release-revision', 'integration-compatible-release') - const refused = await run(args) - expect(refused.code).toBe(1) - expect(refused.stdout + refused.stderr).toContain('Apply requires') - expect(await sql`SELECT * FROM project`).toHaveLength(0) - } - - expect( - ( - await run([ - 'apply', - '--from-file', - planPath, - '--writers-drained', - '--project-writers-enabled', - '--release-revision', - 'integration-compatible-release', - '--database-id', - 'wrong', - ]) - ).code - ).toBe(1) - expect(await sql`SELECT * FROM project`).toHaveLength(0) - expect( - ( - await run([ - 'apply', - '--from-file', - planPath, - '--writers-drained', - '--project-writers-enabled', - '--release-revision', - 'integration-compatible-release', - '--database-id', - plan.databaseId, - ]) - ).code - ).toBe(0) - const applied = JSON.parse(await readFile(reportPath, 'utf8')) - expect(applied.operatorAssertions).toEqual({ - oldWritersDrained: true, - projectWritersEnabled: true, - releaseRevision: 'integration-compatible-release', - }) - expect((await run(['verify'])).code).toBe(0) - const report = JSON.parse(await readFile(reportPath, 'utf8')) as ProjectBackfillReport - expect(report.ready).toBe(true) - expect( - ( - await run( - [ - 'apply', - '--from-file', - planPath, - '--writers-drained', - '--project-writers-enabled', - '--release-revision', - 'integration-compatible-release', - '--database-id', - plan.databaseId, - ], - planPath - ) - ).code - ).toBe(1) - expect(JSON.parse(await readFile(planPath, 'utf8')).mode).toBe('dry-run') - } finally { - await rm(directory, { recursive: true, force: true }) - } - }) - }) -}) diff --git a/scripts/test-integration.ts b/scripts/test-integration.ts index 1a5e4eee27b..1f7a70f81e6 100644 --- a/scripts/test-integration.ts +++ b/scripts/test-integration.ts @@ -11,6 +11,7 @@ import { generateId } from '@sim/utils/id' * Creates and removes its own Postgres and Redis containers, provisions the schema the way CI does, * and runs `vitest run --mode integration` in `packages/db` and `apps/sim` with `TEST_DATABASE_URL` * and `TEST_REDIS_URL`; it never reads an application DSN. + * Set INTEGRATION_DB_PROVISION=migrate to exercise the versioned SQL migration path. * Set KNOWLEDGE_SCALE_TEST=true for the opt-in scale suite; its JSON report is saved in tmpdir. * Optional positional `apps/sim` Vitest filename filters limit a diagnostic run (and skip * `packages/db`); omit them for full validation. @@ -20,6 +21,9 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const container = `sim-integration-test-${generateId()}` const redisContainer = `${container}-redis` const database = 'sim_test' +const provision = process.env.INTEGRATION_DB_PROVISION ?? 'push' +if (provision !== 'push' && provision !== 'migrate') + throw new Error('INTEGRATION_DB_PROVISION must be push or migrate') const scale = process.env.KNOWLEDGE_SCALE_TEST === 'true' const searchPerformance = process.env.KNOWLEDGE_SEARCH_PERFORMANCE_TEST === 'true' const testFilters = process.argv.slice(2) @@ -158,12 +162,12 @@ try { TEST_REDIS_URL: `redis://${redisEndpoint}`, ...(scale ? { KNOWLEDGE_SCALE_REPORT_FILE: scaleReportFile } : {}), } - run('bun', ['run', 'db:push'], { cwd: path.join(root, 'packages/db'), env: environment }) + run('bun', ['run', `db:${provision}`], { cwd: path.join(root, 'packages/db'), env: environment }) run( - 'bunx', + 'bun', [ - 'vitest', 'run', + 'test', '--mode', 'integration', ...(scale ? ['lib/knowledge/__integration__/scale.integration.ts'] : testFilters), @@ -174,7 +178,7 @@ try { } ) if (!scale && testFilters.length === 0) { - run('bunx', ['vitest', 'run', '--mode', 'integration'], { + run('bun', ['run', 'test', '--mode', 'integration'], { cwd: path.join(root, 'packages/db'), env: environment, }) From 99189f2f643d754d2f80913dceb2dd33a37eda67 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 16:51:48 -0700 Subject: [PATCH 7/8] fix(projects): clean up automatically created fixture Projects --- .../__integration__/search-source-setup.integration.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts b/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts index eee347e4058..3d99262cb32 100644 --- a/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts @@ -6,6 +6,7 @@ import { knowledgeConnector, mcpServers, permissions, + project, resourcePolicy, user, workspace, @@ -96,6 +97,7 @@ describe('Search source identity and concurrent creation', () => { await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) await db.delete(workspace).where(eq(workspace.id, other.workspaceId)) for (const id of [ids.aliceId, ids.bobId, other.aliceId, other.bobId]) { + await db.delete(project).where(eq(project.ownerId, id)) await db.delete(user).where(eq(user.id, id)) } }) From 552ce688c39419a1836fbaebd47828cac4294630 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 2 Oct 2026 18:21:48 -0700 Subject: [PATCH 8/8] fix(workflows): guard restore against concurrent workspace archive --- .../__integration__/foundation.integration.ts | 41 +++++++++++++++++++ apps/sim/lib/workflows/lifecycle.ts | 3 ++ 2 files changed, 44 insertions(+) diff --git a/apps/sim/lib/projects/__integration__/foundation.integration.ts b/apps/sim/lib/projects/__integration__/foundation.integration.ts index 2512f49cb4d..a466df28eaf 100644 --- a/apps/sim/lib/projects/__integration__/foundation.integration.ts +++ b/apps/sim/lib/projects/__integration__/foundation.integration.ts @@ -45,6 +45,7 @@ import { lockWorkspaceProject, transferWorkspaceProjects, } from '@/lib/projects/membership' +import { restoreWorkflow } from '@/lib/workflows/lifecycle' import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' import { createWorkspaceInTransaction } from '@/lib/workspaces/create' import { archiveWorkspace } from '@/lib/workspaces/lifecycle' @@ -838,6 +839,46 @@ describe('Project foundation at the database and application boundary', () => { } ) + check('workflow restore cannot overtake a concurrent Project archive', async () => { + const f = await fixture() + const workflowId = await addWorkflow(f.ids[0], f.ownerId) + await db.update(workflow).set({ archivedAt: new Date() }).where(eq(workflow.id, workflowId)) + const held = createDeferred() + const release = createDeferred() + const archive = db.transaction(async (tx) => { + const [connection] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) + await tx.select().from(workflow).where(eq(workflow.id, workflowId)).for('update') + await archiveProjectInTransaction(tx, f.projectId) + held.resolve(connection.pid) + await release.promise + }) + const blocker = await held.promise + const restore = restoreWorkflow(workflowId, request).then( + (result) => result, + (error: unknown) => error + ) + try { + let waiting = false + for (let attempt = 0; attempt < 100; attempt++) { + const rows = await db.execute(sql` + SELECT 1 FROM pg_stat_activity WHERE ${blocker} = ANY(pg_blocking_pids(pid)) + `) + if (rows.length) { + waiting = true + break + } + await sleep(10) + } + expect(waiting).toBe(true) + } finally { + release.resolve() + await archive + } + expect(await restore).toMatchObject({ code: 'not_found' }) + const [row] = await db.select().from(workflow).where(eq(workflow.id, workflowId)) + expect(row.archivedAt).not.toBeNull() + }) + check( 'Project archive rolls back as a unit and retries without losing environments', async () => { diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index 27e59772f40..d7a73c8fb7c 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -22,6 +22,7 @@ import { mcpPubSub } from '@/lib/mcp/pubsub' import { releaseWebhookPathClaims } from '@/lib/webhooks/path-claims' import { supersedeInFlightDeploymentOperations } from '@/lib/workflows/persistence/deployment-operations' import { getWorkflowById } from '@/lib/workflows/utils' +import { lockActiveWorkspace } from '@/lib/workspaces/active-workspace' const logger = createLogger('WorkflowLifecycle') @@ -178,6 +179,8 @@ export async function restoreWorkflow( const archivedAt = existingWorkflow.archivedAt await db.transaction(async (tx) => { + if (existingWorkflow.workspaceId) await lockActiveWorkspace(tx, existingWorkflow.workspaceId) + await tx .update(workflow) .set({