From 5327ce89865e6f8c20b41315c930b8b2c02a0921 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 17:47:08 -0700 Subject: [PATCH 01/10] feat(files): workspace file version history over the v2 API, CLI, and MCP --- .github/workflows/test-build.yml | 6 + apps/docs/content/docs/cli/files.mdx | 157 ++ apps/docs/content/docs/cli/reference.mdx | 169 +++ .../platform/enterprise/data-retention.mdx | 11 +- .../docs/platform/enterprise/self-hosted.mdx | 3 +- apps/docs/openapi-v2-files-audit.json | 1270 ++++++++++++++++- .../api/cron/cleanup-file-versions/route.ts | 26 + apps/sim/app/api/files/authorization.test.ts | 42 +- apps/sim/app/api/files/authorization.ts | 17 + .../[id]/data-retention/route.ts | 5 + .../[id]/data-retention/route.ts | 7 +- .../v2/files/[fileId]/metadata/route.test.ts | 18 +- .../api/v2/files/[fileId]/metadata/route.ts | 8 +- .../versions/[version]/content/route.ts | 38 + .../versions/[version]/revert/route.ts | 34 + .../[fileId]/versions/[version]/route.ts | 47 + .../[fileId]/versions/[version]/text/route.ts | 54 + .../api/v2/files/[fileId]/versions/route.ts | 50 + apps/sim/app/api/v2/files/utils.ts | 34 + apps/sim/background/cleanup-file-versions.ts | 190 +++ apps/sim/background/cleanup-soft-deletes.ts | 52 +- .../components/data-retention-settings.tsx | 47 +- apps/sim/hooks/queries/workspace-files.ts | 5 +- apps/sim/lib/api/contracts/deployments.ts | 30 +- apps/sim/lib/api/contracts/organization.ts | 2 + apps/sim/lib/api/contracts/primitives.ts | 23 + .../v2/__tests__/cross-cutting.test.ts | 7 +- .../v2/__tests__/list-pagination.test.ts | 7 + .../sim/lib/api/contracts/v2/file-versions.ts | 251 ++++ apps/sim/lib/api/contracts/v2/files.ts | 7 + .../api/contracts/v2/openapi/files-audit.ts | 241 +++- .../lib/api/mcp/generated/v2-operations.ts | 52 +- apps/sim/lib/billing/cleanup-dispatcher.ts | 12 +- apps/sim/lib/billing/retention.ts | 1 + apps/sim/lib/cleanup/storage-delete.ts | 26 + apps/sim/lib/collab-doc/persist.ts | 1 + .../core/async-jobs/backends/trigger-dev.ts | 1 + apps/sim/lib/core/async-jobs/types.ts | 1 + apps/sim/lib/internal/file/operations.test.ts | 4 + .../file-versions.integration.ts | 431 ++++++ .../workspace/workspace-file-manager.ts | 177 ++- .../workspace-file-secret-provenance.test.ts | 32 +- .../workspace-file-secret-provenance.ts | 85 +- .../workspace-file-storage-accounting.test.ts | 119 +- ...kspace-file-storage-cleanup-outbox.test.ts | 43 +- .../workspace-file-storage-cleanup-outbox.ts | 42 +- .../workspace/workspace-file-versions.ts | 447 ++++++ apps/sim/lib/uploads/core/errors.ts | 13 + apps/sim/lib/users/account-deletion.ts | 18 + .../application/download-workspace-file.ts | 17 +- .../edit-workspace-file-content.ts | 2 + .../application/file-version-write.ts | 36 + .../application/file-versions.test.ts | 345 +++++ .../application/file-versions.ts | 377 +++++ .../workspace-files/application/operations.ts | 64 +- .../read-workspace-file-metadata.ts | 21 + .../application/read-workspace-file-text.ts | 13 + .../update-workspace-file-content.ts | 2 + packages/audit/src/types.ts | 4 + packages/db/schema.ts | 82 ++ .../src/commands/protocol/files-get.ts | 114 +- .../sim-cli/src/commands/protocol/index.ts | 3 +- packages/sim-cli/src/contract/commands.ts | 52 + packages/sim-cli/src/generated/v2-api.ts | 297 ++++ packages/sim-cli/src/http/client.test.ts | 2 + packages/testing/src/mocks/audit.mock.ts | 2 + packages/testing/src/mocks/schema.mock.ts | 19 + scripts/check-api-validation-contracts.ts | 1 + scripts/openapi/documents.test.ts | 4 +- 69 files changed, 5568 insertions(+), 252 deletions(-) create mode 100644 apps/sim/app/api/cron/cleanup-file-versions/route.ts create mode 100644 apps/sim/app/api/v2/files/[fileId]/versions/[version]/content/route.ts create mode 100644 apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.ts create mode 100644 apps/sim/app/api/v2/files/[fileId]/versions/[version]/route.ts create mode 100644 apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts create mode 100644 apps/sim/app/api/v2/files/[fileId]/versions/route.ts create mode 100644 apps/sim/background/cleanup-file-versions.ts create mode 100644 apps/sim/lib/api/contracts/v2/file-versions.ts create mode 100644 apps/sim/lib/cleanup/storage-delete.ts create mode 100644 apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts create mode 100644 apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts create mode 100644 apps/sim/lib/workspace-files/application/file-version-write.ts create mode 100644 apps/sim/lib/workspace-files/application/file-versions.test.ts create mode 100644 apps/sim/lib/workspace-files/application/file-versions.ts diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index a890cd09d0b..323e8bbdc3d 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -149,6 +149,12 @@ jobs: KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim run: bunx vitest run --mode integration lib/workspace-files/search/dispatcher.integration.ts + - name: Verify workspace file version history on PostgreSQL 17 + working-directory: apps/sim + env: + KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim + run: bunx vitest run --mode integration lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts + - name: Verify SCIM and administration over real HTTP working-directory: apps/sim env: diff --git a/apps/docs/content/docs/cli/files.mdx b/apps/docs/content/docs/cli/files.mdx index e952bf299b6..41f15299483 100644 --- a/apps/docs/content/docs/cli/files.mdx +++ b/apps/docs/content/docs/cli/files.mdx @@ -174,6 +174,163 @@ sim files delete [options] +## Permanently delete a previous version of a file + +```bash +sim files versions delete [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-y, --yes` | Yes | Confirm this operation. | + + + +## Show the metadata of one version of a file + +```bash +sim files versions describe +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +## List the recorded versions of a file + +```bash +sim files versions list [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--sort-by ` | No | Field used to sort the result. Accepted values: `version`. | +| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. | +| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. | +| `--cursor ` | No | Continue from nextCursor returned by a previous result. | + + + +## Read the text content of one version of a file + +```bash +sim files versions read [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--max-bytes ` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. | +| `--offset ` | No | First line to return, 1-based. Absent starts at the first line. | +| `--limit ` | No | How many lines to return from `offset`. Absent reads to the end. | + + + +## Make a previous version of a file current again + +```bash +sim files versions revert [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--expected-current-version ` | No | Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number. | + + + +## Download the content of one version of a file + +```bash +sim files versions download [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-o, --output-file ` | No | Write content to a file instead of stdout. | +| `--force` | No | Overwrite --output-file if it already exists. | + + + ## Apply one exact or anchor-based edit to a text file ```bash diff --git a/apps/docs/content/docs/cli/reference.mdx b/apps/docs/content/docs/cli/reference.mdx index 0a92390a38c..49d8a095210 100644 --- a/apps/docs/content/docs/cli/reference.mdx +++ b/apps/docs/content/docs/cli/reference.mdx @@ -909,6 +909,175 @@ sim files delete [options] +### sim files versions delete + +Permanently delete a previous version of a file + +```bash +sim files versions delete [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-y, --yes` | Yes | Confirm this operation. | + + + +### sim files versions describe + +Show the metadata of one version of a file + +```bash +sim files versions describe +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +### sim files versions list + +List the recorded versions of a file + +```bash +sim files versions list [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--sort-by ` | No | Field used to sort the result. Accepted values: `version`. | +| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. | +| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. | +| `--cursor ` | No | Continue from nextCursor returned by a previous result. | + + + +### sim files versions read + +Read the text content of one version of a file + +```bash +sim files versions read [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--max-bytes ` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. | +| `--offset ` | No | First line to return, 1-based. Absent starts at the first line. | +| `--limit ` | No | How many lines to return from `offset`. Absent reads to the end. | + + + +### sim files versions revert + +Make a previous version of a file current again + +```bash +sim files versions revert [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `--expected-current-version ` | No | Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number. | + + + +### sim files versions download + +Download the content of one version of a file + +```bash +sim files versions download [options] +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `fileId` | Yes | File identifier. | +| `version` | Yes | Version number. | + + + +**Options** + + + +| Option | Required | Description | +| --- | --- | --- | +| `-o, --output-file ` | No | Write content to a file instead of stdout. | +| `--force` | No | Overwrite --output-file if it already exists. | + + + ### sim files edit Apply one exact or anchor-based edit to a text file diff --git a/apps/docs/content/docs/platform/enterprise/data-retention.mdx b/apps/docs/content/docs/platform/enterprise/data-retention.mdx index aa3fbe8b578..4b57215fced 100644 --- a/apps/docs/content/docs/platform/enterprise/data-retention.mdx +++ b/apps/docs/content/docs/platform/enterprise/data-retention.mdx @@ -71,6 +71,12 @@ Controls how long **Chat data** is kept, including: - Run checkpoints and async tool calls - Inbox tasks +### Previous file versions + +Every change to a file's content keeps the previous content as a version you can read or revert to through the API, CLI, or MCP server. This setting controls how long a version is kept after a newer one replaces it. The newest ten versions of each file are always kept, whatever their age. + +Without a setting, previous versions are kept until a file reaches 500 of them. The setting isn't on the settings page yet: set `fileVersionRetentionHours` through the data retention API, either for the organization or in a workspace override. + Each setting is independent. You can configure a short log retention period alongside a long soft deletion cleanup period, or any combination that fits your compliance requirements. --- @@ -186,16 +192,17 @@ Once enabled, retention settings are configurable through **Settings → Organiz ### Scheduling the deletion pass -`DATA_RETENTION_ENABLED` permits deletion; it does not perform it. Deletion runs when a scheduled request reaches one of three endpoints, each authenticated with a bearer token equal to `CRON_SECRET`: +`DATA_RETENTION_ENABLED` permits deletion; it does not perform it. Deletion runs when a scheduled request reaches one of four endpoints, each authenticated with a bearer token equal to `CRON_SECRET`: | Category | Endpoint | |----------|----------| | Execution and job logs | `GET /api/logs/cleanup` | | Soft-deleted resources | `GET /api/cron/cleanup-soft-deletes` | | Chats and Chat runs | `GET /api/cron/cleanup-tasks` | +| Previous file versions | `GET /api/cron/cleanup-file-versions` | -Neither shipped deployment schedules these three endpoints — not the Helm chart, not Docker Compose's `cron` service. An operator who sets `DATA_RETENTION_ENABLED=true` alone still deletes nothing. Add them to `cronjobs.jobs`, or call them daily from an external scheduler. +Neither shipped deployment schedules these four endpoints — not the Helm chart, not Docker Compose's `cron` service. An operator who sets `DATA_RETENTION_ENABLED=true` alone still deletes nothing. Add them to `cronjobs.jobs`, or call them daily from an external scheduler. ```bash diff --git a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx index c623ecd169c..7de297e137a 100644 --- a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx +++ b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx @@ -86,10 +86,11 @@ Persist that value as `CRON_SECRET` on the app **and** on whatever calls these e | Retention — logs | `GET /api/logs/cleanup` | Daily | **No** — schedule it yourself | | Retention — soft deletes | `GET /api/cron/cleanup-soft-deletes` | Daily | **No** — schedule it yourself | | Retention — Chat tasks | `GET /api/cron/cleanup-tasks` | Daily | **No** — schedule it yourself | +| Retention — file versions | `GET /api/cron/cleanup-file-versions` | Daily | **No** — schedule it yourself | | OAuth token cleanup | `GET /api/cron/cleanup-oauth-tokens` | Hourly | Yes — Helm and Docker Compose both call it | - Both shipped deployments schedule the data-drain dispatcher and OAuth token cleanup, but **not** the three configurable data-retention endpoints. Setting `DATA_RETENTION_ENABLED=true` alone deletes no retained product data — those windows are evaluated only when one of the three endpoints is called. Add them to `cronjobs.jobs` yourself, or drive them from an external scheduler. + Both shipped deployments schedule the data-drain dispatcher and OAuth token cleanup, but **not** the four configurable data-retention endpoints. Setting `DATA_RETENTION_ENABLED=true` alone deletes no retained product data — those windows are evaluated only when one of the four endpoints is called. Add them to `cronjobs.jobs` yourself, or drive them from an external scheduler. OAuth token cleanup runs independently of sign-in activity, removing expired and revoked credentials. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim) for provider configuration. diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index 66a916f5767..11411b96bb9 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -23,7 +23,7 @@ "tags": [ { "name": "Files", - "description": "Create, upload, download, organize, share, and delete workspace files." + "description": "Create, upload, download, organize, share, version, and delete workspace files." }, { "name": "Audit Logs", @@ -864,6 +864,667 @@ } } }, + "/api/v2/files/{fileId}/versions": { + "get": { + "operationId": "listFileVersions", + "summary": "List File Versions", + "description": "List the recorded versions of a file, newest first by default. Every content write records a version; collaborative edits and repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. Retention removes old versions by plan, always keeping the newest ten, so version numbers can have gaps.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "files.versions.list", + "x-oauth-scope": "api:read", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "description": "Workspace that owns the file.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + } + }, + { + "name": "sortBy", + "in": "query", + "required": false, + "description": "Field used to sort the result.", + "schema": { + "default": "version", + "description": "Field used to sort the result.", + "type": "string", + "enum": ["version"] + } + }, + { + "name": "sortOrder", + "in": "query", + "required": false, + "description": "Sort direction.", + "schema": { + "default": "desc", + "description": "Sort direction.", + "type": "string", + "enum": ["asc", "desc"] + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum versions to return per page. Must be a whole number from 1 to 100. Defaults to 50.", + "schema": { + "default": 50, + "description": "Maximum versions to return per page. Must be a whole number from 1 to 100. Defaults to 50.", + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.", + "schema": { + "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.", + "type": "string", + "minLength": 1 + } + } + ], + "responses": { + "200": { + "description": "A page of file versions.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2FileVersionListResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/files/{fileId}/versions/{version}": { + "get": { + "operationId": "getFileVersion", + "summary": "Get File Version", + "description": "Get one version of a file. A version removed by retention, or one that never existed, returns `404`.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "files.versions.read", + "x-oauth-scope": "api:read", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version number.", + "schema": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647, + "description": "Version number." + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "description": "Workspace that owns the file.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + } + } + ], + "responses": { + "200": { + "description": "The file version.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2FileVersionResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + }, + "delete": { + "operationId": "deleteFileVersion", + "summary": "Delete File Version", + "description": "Permanently delete one earlier version and its stored content, for example to purge a leaked value from history before retention removes it. The current version returns `409`; revert to another version first. A version that does not exist returns `404`.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "files.versions.delete", + "x-oauth-scope": "api:write", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version number.", + "schema": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647, + "description": "Version number." + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "description": "Workspace that owns the file.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + } + } + ], + "responses": { + "200": { + "description": "Deletion confirmation.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2FileVersionDeleteResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/files/{fileId}/versions/{version}/text": { + "get": { + "operationId": "readFileVersionText", + "summary": "Read File Version Text", + "description": "Extract the text of one version, exactly as Read File Text extracts the current content. Unsupported types return `400`, compiling documents return `409`, and oversized versions return `413`.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "files.versions.read_content", + "x-oauth-scope": "api:read", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version number.", + "schema": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647, + "description": "Version number." + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "description": "Workspace that owns the file.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + } + }, + { + "name": "maxBytes", + "in": "query", + "required": false, + "description": "Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit.", + "schema": { + "description": "Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit.", + "type": "integer", + "minimum": 1, + "maximum": 26214400 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "First line to return, 1-based. Absent starts at the first line.", + "schema": { + "description": "First line to return, 1-based. Absent starts at the first line.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "How many lines to return from `offset`. Absent reads to the end.", + "schema": { + "description": "How many lines to return from `offset`. Absent reads to the end.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "The extracted text of the version and its extraction-quality flags.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileVersionTextResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/files/{fileId}/versions/{version}/content": { + "get": { + "operationId": "downloadFileVersion", + "summary": "Download File Version", + "description": "Download the bytes of one version, served exactly as Download File serves the current bytes. Generated documents use compiled artifacts, returning `409` while compiling and `413` above the rendered-size ceiling. Downloading records an audit event. `HEAD` checks access with the same authorization as `GET` but skips side effects, returning an empty `200` without payload headers on success. `HEAD` omits `Content-Length`; use file metadata to size downloads.\n\nOAuth scope: `api:read`.", + "x-sim-operation": "files.versions.download", + "x-oauth-scope": "api:read", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version number.", + "schema": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647, + "description": "Version number." + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "description": "Workspace that owns the file.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + } + } + ], + "responses": { + "200": { + "description": "The version bytes.", + "headers": { + "Content-Type": { + "$ref": "#/components/headers/Content-Type" + }, + "Content-Disposition": { + "$ref": "#/components/headers/Content-Disposition" + }, + "Content-Length": { + "$ref": "#/components/headers/Content-Length" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, + "/api/v2/files/{fileId}/versions/{version}/revert": { + "post": { + "operationId": "revertFileVersion", + "summary": "Revert File Version", + "description": "Make the content of a version current again by writing it as a new `revert` version, so the revert can itself be reverted. Open editors receive the change. Reverting to the current version writes nothing and returns `reverted: false`. A concurrent write, or an `expectedCurrentVersion` that is no longer current, returns `409`; a version above 100 MB returns `413`.\n\nOAuth scope: `api:write`.", + "x-sim-operation": "files.versions.revert", + "x-oauth-scope": "api:write", + "tags": ["Files"], + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "File identifier.", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File identifier." + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version number.", + "schema": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647, + "description": "Version number." + } + } + ], + "requestBody": { + "required": true, + "description": "Workspace scope and an optional current-version precondition.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevertFileVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The file and its current version after the revert.", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2FileVersionRevertResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + } + } + } + }, "/api/v2/files/bulk-download": { "get": { "operationId": "bulkDownloadFiles", @@ -1407,7 +2068,7 @@ "get": { "operationId": "getFile", "summary": "Get File Metadata", - "description": "Get file metadata and its public-share configuration. The `share` field is null when the file has never been shared.\n\nOAuth scope: `api:read`.", + "description": "Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.", "x-sim-operation": "files.read_metadata", "x-oauth-scope": "api:read", "tags": ["Files"], @@ -3658,114 +4319,475 @@ }, "required": ["workspaceId", "name", "contentType", "size"], "additionalProperties": false, - "title": "Create file upload request", - "description": "File metadata required to create an upload session." + "title": "Create file upload request", + "description": "File metadata required to create an upload session." + }, + "FileUploadResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2FileUpload" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "File upload response", + "description": "Current upload-session state." + }, + "V2UploadPartUrl": { + "type": "object", + "properties": { + "partNumber": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Multipart part number." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Signed URL for this upload part. Upload bytes with `PUT` and exactly the supplied headers; never construct or modify the signed URL. Treat any `2xx` as success. Sim-hosted URLs return an empty `204` and v2 JSON errors. Object-storage URLs may return `200` or `201` and provider-specific errors, often XML. Do not retain part `ETag` values; after every part succeeds, call the completion endpoint without a request body." + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + }, + "description": "Headers that must be included with the part upload." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 expiration time for the signed URL." + } + }, + "required": ["partNumber", "url", "headers", "expiresAt"], + "additionalProperties": false, + "title": "Upload part URL", + "description": "A signed URL and required headers for one multipart upload part." + }, + "V2PartUrlsData": { + "type": "object", + "properties": { + "parts": { + "maxItems": 100, + "type": "array", + "items": { + "$ref": "#/components/schemas/V2UploadPartUrl" + }, + "description": "Signed URLs for requested parts." + } + }, + "required": ["parts"], + "additionalProperties": false, + "title": "Upload part URLs", + "description": "Signed transfer URLs for the requested multipart upload parts." + }, + "CreateFileUploadPartUrlsResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2PartUrlsData" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Create upload part URLs response", + "description": "Signed multipart upload URLs." + }, + "CreateFileUploadPartUrlsRequest": { + "type": "object", + "properties": { + "partNumbers": { + "minItems": 1, + "maxItems": 100, + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "description": "Multipart part numbers for which signed URLs should be created." + } + }, + "required": ["partNumbers"], + "additionalProperties": false, + "title": "Create upload part URLs request", + "description": "Multipart part numbers requiring signed URLs.", + "examples": [ + { + "partNumbers": [1, 2] + } + ] + }, + "V2FileText": { + "type": "object", + "properties": { + "fileId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9_-]+$", + "description": "File the text was extracted from." + }, + "name": { + "type": "string", + "description": "File name, including its extension." + }, + "type": { + "type": "string", + "description": "Stored MIME type of the source file." + }, + "text": { + "type": "string", + "description": "Extracted text." + }, + "truncated": { + "type": "boolean", + "description": "True when a parser limit stopped extraction before the input was exhausted." + }, + "degraded": { + "type": "boolean", + "description": "True when text extraction did not fully succeed and `text` may be incomplete or synthesized from the raw bytes rather than read from the document. Never treat degraded text as authoritative content." + }, + "degradedReason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Why extraction degraded, or null when it did not." + }, + "charCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Length of `text` in characters." + }, + "byteCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Source bytes read from storage before extraction." + }, + "lineRange": { + "description": "Present when `offset` or `limit` narrowed the response. `totalLines` is what separates a file that ended from a window that stopped early.", + "type": "object", + "properties": { + "offset": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "First line returned, 1-based." + }, + "lineCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Lines returned." + }, + "totalLines": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Lines the whole file holds." + }, + "totalLinesExact": { + "type": "boolean", + "description": "False when text extraction was truncated, so `totalLines` counts only the extracted prefix and is not the end of the file." + } + }, + "required": ["offset", "lineCount", "totalLines", "totalLinesExact"], + "additionalProperties": false + } + }, + "required": [ + "fileId", + "name", + "type", + "text", + "truncated", + "degraded", + "degradedReason", + "charCount", + "byteCount" + ], + "additionalProperties": false, + "title": "Extracted file text", + "description": "Text extracted from a workspace file, with extraction-quality flags." }, - "FileUploadResponse": { + "FileTextResponse": { "type": "object", "properties": { "data": { "description": "Response data.", - "$ref": "#/components/schemas/V2FileUpload" + "$ref": "#/components/schemas/V2FileText" } }, "required": ["data"], "additionalProperties": false, - "title": "File upload response", - "description": "Current upload-session state." + "title": "File text response", + "description": "Text extracted from a workspace file." }, - "V2UploadPartUrl": { + "V2FileVersion": { "type": "object", "properties": { - "partNumber": { + "fileId": { + "type": "string", + "description": "File this version belongs to." + }, + "version": { "type": "integer", - "minimum": 1, + "exclusiveMinimum": 0, "maximum": 9007199254740991, - "description": "Multipart part number." + "description": "Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.", + "examples": [3] }, - "url": { + "isCurrent": { + "type": "boolean", + "description": "Whether this version holds the current content of the file." + }, + "size": { + "type": "number", + "minimum": 0, + "description": "Size in bytes of the stored content of this version." + }, + "contentType": { "type": "string", - "format": "uri", - "description": "Signed URL for this upload part. Upload bytes with `PUT` and exactly the supplied headers; never construct or modify the signed URL. Treat any `2xx` as success. Sim-hosted URLs return an empty `204` and v2 JSON errors. Object-storage URLs may return `200` or `201` and provider-specific errors, often XML. Do not retain part `ETag` values; after every part succeeds, call the completion endpoint without a request body." + "description": "MIME type of the stored content of this version." }, - "headers": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" + "source": { + "type": "string", + "enum": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"], + "description": "What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed)." + }, + "authors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User identifier." + }, + "email": { + "anyOf": [ + { + "type": "string", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + }, + { + "type": "null" + } + ], + "description": "Current email address of the user, or null when the account no longer exists." + } + }, + "required": ["id", "email"], + "additionalProperties": false }, - "description": "Headers that must be included with the part upload." + "description": "Users who wrote this version, in order of first contribution. Empty for actorless writers such as workspace API keys. A collaborative version lists every editor in its window." }, - "expiresAt": { + "restoredFromVersion": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ], + "description": "For a `revert` version, the version whose content it restored; otherwise null." + }, + "createdAt": { "type": "string", + "description": "ISO 8601 timestamp when this content became current.", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", - "description": "ISO 8601 expiration time for the signed URL." + "examples": ["2026-01-15T10:30:00Z"] + }, + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the last write folded into this version. Equals `createdAt` unless edits were coalesced into it.", + "format": "date-time", + "examples": ["2026-01-15T10:38:00Z"] + }, + "supersededAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp when a newer version replaced this one, or null for the current version. Retention ages versions from this time.", + "format": "date-time", + "examples": ["2026-01-16T09:00:00Z"] } }, - "required": ["partNumber", "url", "headers", "expiresAt"], + "required": [ + "fileId", + "version", + "isCurrent", + "size", + "contentType", + "source", + "authors", + "restoredFromVersion", + "createdAt", + "updatedAt", + "supersededAt" + ], "additionalProperties": false, - "title": "Upload part URL", - "description": "A signed URL and required headers for one multipart upload part." + "title": "File version", + "description": "One recorded version of the content of a workspace file." }, - "V2PartUrlsData": { + "V2FileVersionListResponse": { "type": "object", "properties": { - "parts": { - "maxItems": 100, + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/V2UploadPartUrl" + "$ref": "#/components/schemas/V2FileVersion" }, - "description": "Signed URLs for requested parts." + "description": "Items in the current page." + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself." } }, - "required": ["parts"], + "required": ["data", "nextCursor"], "additionalProperties": false, - "title": "Upload part URLs", - "description": "Signed transfer URLs for the requested multipart upload parts." + "title": "File version list response", + "description": "A cursor-paginated page of file versions.", + "examples": [ + { + "data": [ + { + "fileId": "wf_V1StGXR8z5jdHi6BmyT91", + "version": 3, + "isCurrent": true, + "size": 1024, + "contentType": "text/csv", + "source": "api", + "authors": [ + { + "id": "usr_4kJ9mN2pQ7rS", + "email": "jane@example.com" + } + ], + "restoredFromVersion": null, + "createdAt": "2026-01-15T10:30:00Z", + "updatedAt": "2026-01-15T10:30:00Z", + "supersededAt": null + } + ], + "nextCursor": null + } + ] }, - "CreateFileUploadPartUrlsResponse": { + "V2FileVersionResponse": { "type": "object", "properties": { "data": { "description": "Response data.", - "$ref": "#/components/schemas/V2PartUrlsData" + "$ref": "#/components/schemas/V2FileVersion" } }, "required": ["data"], "additionalProperties": false, - "title": "Create upload part URLs response", - "description": "Signed multipart upload URLs." + "title": "File version response", + "description": "A single file version.", + "examples": [ + { + "data": { + "fileId": "wf_V1StGXR8z5jdHi6BmyT91", + "version": 3, + "isCurrent": true, + "size": 1024, + "contentType": "text/csv", + "source": "api", + "authors": [ + { + "id": "usr_4kJ9mN2pQ7rS", + "email": "jane@example.com" + } + ], + "restoredFromVersion": null, + "createdAt": "2026-01-15T10:30:00Z", + "updatedAt": "2026-01-15T10:30:00Z", + "supersededAt": null + } + } + ] }, - "CreateFileUploadPartUrlsRequest": { + "V2FileVersionDeleteResult": { "type": "object", "properties": { - "partNumbers": { - "minItems": 1, - "maxItems": 100, - "type": "array", - "items": { - "type": "integer", - "minimum": 1, - "maximum": 9007199254740991 - }, - "description": "Multipart part numbers for which signed URLs should be created." + "fileId": { + "type": "string", + "description": "File whose version was deleted." + }, + "version": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "Version number that was deleted." + }, + "deleted": { + "type": "boolean", + "const": true, + "description": "Always true: the version and its stored content are gone." } }, - "required": ["partNumbers"], + "required": ["fileId", "version", "deleted"], "additionalProperties": false, - "title": "Create upload part URLs request", - "description": "Multipart part numbers requiring signed URLs.", + "title": "Delete file version result", + "description": "Deletion acknowledgement for one file version." + }, + "V2FileVersionDeleteResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2FileVersionDeleteResult" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Delete file version response", + "description": "Deletion confirmation for one file version.", "examples": [ { - "partNumbers": [1, 2] + "data": { + "fileId": "wf_V1StGXR8z5jdHi6BmyT91", + "version": 2, + "deleted": true + } } ] }, - "V2FileText": { + "V2FileVersionText": { "type": "object", "properties": { "fileId": { @@ -3847,6 +4869,12 @@ }, "required": ["offset", "lineCount", "totalLines", "totalLinesExact"], "additionalProperties": false + }, + "version": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "Version the text was extracted from." } }, "required": [ @@ -3858,24 +4886,123 @@ "degraded", "degradedReason", "charCount", - "byteCount" + "byteCount", + "version" ], "additionalProperties": false, - "title": "Extracted file text", - "description": "Text extracted from a workspace file, with extraction-quality flags." + "title": "Extracted file version text", + "description": "Text extracted from one version of a workspace file, with extraction-quality flags." }, - "FileTextResponse": { + "FileVersionTextResponse": { "type": "object", "properties": { "data": { "description": "Response data.", - "$ref": "#/components/schemas/V2FileText" + "$ref": "#/components/schemas/V2FileVersionText" } }, "required": ["data"], "additionalProperties": false, - "title": "File text response", - "description": "Text extracted from a workspace file." + "title": "File version text response", + "description": "Text extracted from one version of a workspace file." + }, + "V2FileVersionRevertResult": { + "type": "object", + "properties": { + "reverted": { + "type": "boolean", + "description": "False when the requested version was already current, in which case nothing was written." + }, + "file": { + "$ref": "#/components/schemas/V2File" + }, + "version": { + "description": "The current version of the file after the revert: a new `revert` version; the requested version when it was already current; or the unchanged current version when its content already matched the requested one.", + "$ref": "#/components/schemas/V2FileVersion" + } + }, + "required": ["reverted", "file", "version"], + "additionalProperties": false, + "title": "Revert file version result", + "description": "The file and its current version after a revert." + }, + "V2FileVersionRevertResponse": { + "type": "object", + "properties": { + "data": { + "description": "Response data.", + "$ref": "#/components/schemas/V2FileVersionRevertResult" + } + }, + "required": ["data"], + "additionalProperties": false, + "title": "Revert file version response", + "description": "The file and its current version after the revert.", + "examples": [ + { + "data": { + "reverted": true, + "file": { + "id": "wf_V1StGXR8z5jdHi6BmyT91", + "webUrl": "https://www.sim.ai/workspace/a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64/files/wf_V1StGXR8z5jdHi6BmyT91", + "name": "data.csv", + "size": 1024, + "type": "text/csv", + "key": "workspace/example/data.csv", + "folderPath": "/Engineering", + "uploadedByEmail": "jane@example.com", + "uploadedAt": "2026-01-15T10:30:00Z", + "updatedAt": "2026-01-15T10:30:00Z", + "deletedAt": null + }, + "version": { + "fileId": "wf_V1StGXR8z5jdHi6BmyT91", + "version": 5, + "isCurrent": true, + "size": 1024, + "contentType": "text/csv", + "source": "revert", + "authors": [ + { + "id": "usr_4kJ9mN2pQ7rS", + "email": "jane@example.com" + } + ], + "restoredFromVersion": 3, + "createdAt": "2026-01-15T10:30:00Z", + "updatedAt": "2026-01-15T10:30:00Z", + "supersededAt": null + } + } + } + ] + }, + "RevertFileVersionRequest": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Workspace that owns the file." + }, + "expectedCurrentVersion": { + "description": "Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number.", + "type": "integer", + "minimum": 1, + "maximum": 2147483647 + } + }, + "required": ["workspaceId"], + "additionalProperties": false, + "title": "Revert file version request", + "description": "Workspace scope and an optional current-version precondition.", + "examples": [ + { + "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64", + "expectedCurrentVersion": 4 + } + ] }, "V2FileUnzipResult": { "type": "object", @@ -4186,6 +5313,12 @@ } ], "description": "Current public-share state, or null when the file has never been shared." + }, + "currentVersion": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "Version number of the current content. List File Versions returns the history; pass this as `expectedCurrentVersion` to revert only if nothing changed since." } }, "required": [ @@ -4200,7 +5333,8 @@ "uploadedAt", "updatedAt", "deletedAt", - "share" + "share", + "currentVersion" ], "additionalProperties": false, "title": "File metadata", @@ -4232,7 +5366,8 @@ "uploadedAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z", "deletedAt": null, - "share": null + "share": null, + "currentVersion": 1 } }, { @@ -4258,7 +5393,8 @@ "authType": "public", "hasPassword": false, "allowedEmails": [] - } + }, + "currentVersion": 3 } } ] diff --git a/apps/sim/app/api/cron/cleanup-file-versions/route.ts b/apps/sim/app/api/cron/cleanup-file-versions/route.ts new file mode 100644 index 00000000000..a4b312347ce --- /dev/null +++ b/apps/sim/app/api/cron/cleanup-file-versions/route.ts @@ -0,0 +1,26 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('FileVersionCleanupAPI') + +/** GET /api/cron/cleanup-file-versions — dispatch retention for superseded workspace file versions. */ +export const GET = withRouteHandler(async (request: NextRequest) => { + try { + const authError = verifyCronAuth(request, 'file version cleanup') + if (authError) return authError + + const result = await dispatchCleanupJobs('cleanup-file-versions') + + logger.info('File version cleanup jobs dispatched', result) + + return NextResponse.json({ triggered: true, ...result }) + } catch (error) { + logger.error('Failed to dispatch file version cleanup jobs:', { error }) + return NextResponse.json({ error: 'Failed to dispatch file version cleanup' }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/files/authorization.test.ts b/apps/sim/app/api/files/authorization.test.ts index e9771609833..3121691a068 100644 --- a/apps/sim/app/api/files/authorization.test.ts +++ b/apps/sim/app/api/files/authorization.test.ts @@ -18,13 +18,21 @@ import { } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetFileMetadataByKey, mockGetUserEntityPermissions, mockGetFileMetadata } = vi.hoisted( - () => ({ - mockGetFileMetadataByKey: vi.fn(), - mockGetUserEntityPermissions: vi.fn(), - mockGetFileMetadata: vi.fn(), - }) -) +const { + mockGetFileMetadataByKey, + mockGetUserEntityPermissions, + mockGetFileMetadata, + mockFindWorkspaceFileVersionKeys, +} = vi.hoisted(() => ({ + mockGetFileMetadataByKey: vi.fn(), + mockGetUserEntityPermissions: vi.fn(), + mockGetFileMetadata: vi.fn(), + mockFindWorkspaceFileVersionKeys: vi.fn(), +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + findWorkspaceFileVersionKeys: mockFindWorkspaceFileVersionKeys, +})) vi.mock('@/lib/uploads', () => ({ getFileMetadata: mockGetFileMetadata, @@ -257,6 +265,7 @@ describe('workspace-scoped access (workspace files and mothership attachments)', // come from the binding itself rather than a fallback happening to grant. dbChainMockFns.limit.mockResolvedValue([]) mockGetFileMetadata.mockResolvedValue({}) + mockFindWorkspaceFileVersionKeys.mockResolvedValue(new Set()) }) function read(cloudKey: string, context: 'workspace' | 'mothership') { @@ -343,6 +352,25 @@ describe('workspace-scoped access (workspace files and mothership attachments)', await expect(read(ATTACHMENT_KEY, 'workspace')).resolves.toBe(false) }) + it('still authorizes an unbound key from its object metadata', async () => { + mockGetFileMetadataByKey.mockResolvedValue(null) + mockGetFileMetadata.mockResolvedValue({ workspaceId: 'ws-1' }) + mockGetUserEntityPermissions.mockResolvedValue('read') + + await expect(read(ATTACHMENT_KEY, 'workspace')).resolves.toBe(true) + }) + + it('denies a retained version key instead of authorizing it from its object metadata', async () => { + mockGetFileMetadataByKey.mockResolvedValue(null) + mockGetFileMetadata.mockResolvedValue({ workspaceId: 'ws-1' }) + mockGetUserEntityPermissions.mockResolvedValue('admin') + mockFindWorkspaceFileVersionKeys.mockResolvedValue(new Set([ATTACHMENT_KEY])) + + await expect(read(ATTACHMENT_KEY, 'workspace')).resolves.toBe(false) + expect(mockFindWorkspaceFileVersionKeys).toHaveBeenCalledWith([ATTACHMENT_KEY]) + expect(mockGetFileMetadata).not.toHaveBeenCalled() + }) + it('does not accept a binding whose context is not workspace-scoped', async () => { bindRow({ workspaceId: 'ws-1', diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index 647e5b5792b..b1dcde923cb 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -13,6 +13,7 @@ import { import { type KnowledgeReadAccess, knowledgeReadAccessBatches } from '@/lib/knowledge/read-access' import { getFileMetadata } from '@/lib/uploads' import type { StorageContext } from '@/lib/uploads/config' +import { findWorkspaceFileVersionKeys } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import type { StorageConfig } from '@/lib/uploads/core/storage-client' import { getFileMetadataByKey } from '@/lib/uploads/server/metadata' import { isWorkspaceScopedContext } from '@/lib/uploads/shared/types' @@ -219,6 +220,18 @@ export async function verifyFileAccess( } } +/** + * A retained file version keeps the storage metadata of the write that created it, so a metadata + * fallback would authorize a replaced or archived file's old bytes by key. Versions are served only + * through the version routes, which authorize against their file, so key-addressed access refuses + * them before any metadata fallback. + */ +async function isRetainedVersionKey(cloudKey: string, userId: string): Promise { + if ((await findWorkspaceFileVersionKeys([cloudKey])).size === 0) return false + logger.warn('File access denied for a retained version key', { userId, cloudKey }) + return true +} + /** * Verify access to workspace files * Priority: Database lookup > Metadata > Deny @@ -269,6 +282,8 @@ async function verifyWorkspaceFileAccess( return false } + if (await isRetainedVersionKey(cloudKey, userId)) return false + // Priority 2: Check metadata (works for both local and cloud files) const config: StorageConfig = customConfig || {} const metadata = await getFileMetadata(cloudKey, config) @@ -745,6 +760,8 @@ async function verifyRegularFileAccess( return false } + if (await isRetainedVersionKey(cloudKey, userId)) return false + // Priority 2: Check metadata (works for both local and cloud files) const config: StorageConfig = customConfig || {} const metadata = await getFileMetadata(cloudKey, config) diff --git a/apps/sim/app/api/organizations/[id]/data-retention/route.ts b/apps/sim/app/api/organizations/[id]/data-retention/route.ts index cb70d83805d..36cc51084b6 100644 --- a/apps/sim/app/api/organizations/[id]/data-retention/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-retention/route.ts @@ -25,6 +25,7 @@ function enterpriseDefaults(): OrganizationRetentionValues { logRetentionHours: CLEANUP_CONFIG['cleanup-logs'].defaults.enterprise, softDeleteRetentionHours: CLEANUP_CONFIG['cleanup-soft-deletes'].defaults.enterprise, taskCleanupHours: CLEANUP_CONFIG['cleanup-tasks'].defaults.enterprise, + fileVersionRetentionHours: CLEANUP_CONFIG['cleanup-file-versions'].defaults.enterprise, piiRedaction: null, retentionOverrides: null, } @@ -37,6 +38,7 @@ function normalizeConfigured( logRetentionHours: settings?.logRetentionHours ?? null, softDeleteRetentionHours: settings?.softDeleteRetentionHours ?? null, taskCleanupHours: settings?.taskCleanupHours ?? null, + fileVersionRetentionHours: settings?.fileVersionRetentionHours ?? null, piiRedaction: settings?.piiRedaction?.rules ? { rules: settings.piiRedaction.rules.map((rule) => ({ @@ -192,6 +194,9 @@ export const PUT = withRouteHandler( if (body.taskCleanupHours !== undefined) { merged.taskCleanupHours = body.taskCleanupHours } + if (body.fileVersionRetentionHours !== undefined) { + merged.fileVersionRetentionHours = body.fileVersionRetentionHours + } if (body.piiRedaction !== undefined) { merged.piiRedaction = body.piiRedaction } diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/data-retention/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/data-retention/route.ts index 97224cb2b25..09657aec72a 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/data-retention/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/data-retention/route.ts @@ -9,8 +9,8 @@ * `DATA_RETENTION_ENABLED` (or `ENTERPRISE_ENABLED`) when billing is off. * * Body: any subset of `logRetentionHours`, `softDeleteRetentionHours`, - * `taskCleanupHours`, `piiRedaction`, `retentionOverrides`. Omitted keys keep - * their current value; `null` means "forever" for an hours field. + * `taskCleanupHours`, `fileVersionRetentionHours`, `piiRedaction`, `retentionOverrides`. + * Omitted keys keep their current value; `null` means "forever" for an hours field. * * Response: AdminSingleResponse<{ success, organizationId }> */ @@ -77,6 +77,9 @@ export const PATCH = withRouteHandler( merged.softDeleteRetentionHours = body.softDeleteRetentionHours } if (body.taskCleanupHours !== undefined) merged.taskCleanupHours = body.taskCleanupHours + if (body.fileVersionRetentionHours !== undefined) { + merged.fileVersionRetentionHours = body.fileVersionRetentionHours + } if (body.piiRedaction !== undefined) { merged.piiRedaction = body.piiRedaction diff --git a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts index 1cc17405ad3..c1eae03d26d 100644 --- a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts @@ -18,7 +18,7 @@ const mocks = vi.hoisted(() => ({ })) vi.mock('@/lib/workspace-files/application/read-workspace-file-metadata', () => ({ - readWorkspaceFileMetadata: { + readWorkspaceFileMetadataWithVersion: { operation: { id: 'files.read_metadata', minimumRole: 'read', workspaceApiKey: 'allow' }, execute: mocks.readMetadata, }, @@ -83,7 +83,11 @@ const callGet = (query: string) => */ const archivedFileUseCase = async ({ input }: { input: { includeDeleted?: boolean } }) => { if (!input.includeDeleted) throw new OrchestrationError('not_found', 'File not found') - return { file: { ...buildRecord(), deletedAt: new Date('2024-01-03T00:00:00Z') }, share: SHARE } + return { + file: { ...buildRecord(), deletedAt: new Date('2024-01-03T00:00:00Z') }, + share: SHARE, + currentVersion: 3, + } } describe('GET /api/v2/files/[fileId]/metadata', () => { @@ -92,7 +96,7 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { v2RouteMocks.authenticate.mockResolvedValue(auth) v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) - mocks.readMetadata.mockResolvedValue({ file: buildRecord(), share: SHARE }) + mocks.readMetadata.mockResolvedValue({ file: buildRecord(), share: SHARE, currentVersion: 3 }) mocks.getUserEmailsByIds.mockResolvedValue(new Map([['user-1', 'ada@example.com']])) }) @@ -141,6 +145,7 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { updatedAt: '2024-01-02T00:00:00.000Z', deletedAt: null, share: SHARE, + currentVersion: 3, }, }) expect(mocks.readMetadata).toHaveBeenCalledWith({ @@ -198,6 +203,7 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { updatedAt: '2024-01-02T00:00:00.000Z', deletedAt: '2024-01-03T00:00:00.000Z', share: SHARE, + currentVersion: 3, }, }) expect(mocks.readMetadata).toHaveBeenCalledWith( @@ -237,7 +243,11 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { }) it('returns a null share when the file has no share configuration', async () => { - mocks.readMetadata.mockResolvedValueOnce({ file: buildRecord(), share: null }) + mocks.readMetadata.mockResolvedValueOnce({ + file: buildRecord(), + share: null, + currentVersion: 3, + }) const response = await callGet(`workspaceId=${WORKSPACE_ID}`) diff --git a/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts b/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts index 722d125e4da..cf6c4ad40e8 100644 --- a/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts +++ b/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts @@ -2,7 +2,7 @@ import { v2GetFileContract } from '@/lib/api/contracts/v2/files' import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' import { v2FileErrorPolicies } from '@/lib/workspace-files/api' import { fileOperations } from '@/lib/workspace-files/application/operations' -import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata' +import { readWorkspaceFileMetadataWithVersion } from '@/lib/workspace-files/application/read-workspace-file-metadata' import { toV2File } from '@/app/api/v2/files/utils' export const dynamic = 'force-dynamic' @@ -28,6 +28,8 @@ export const GET = defineV2JsonRoute({ assertedWorkspaceId: query.workspaceId, includeDeleted: query.scope === 'archived', }), - useCase: readWorkspaceFileMetadata, - present: async ({ file, share }) => ({ data: { ...(await toV2File(file)), share } }), + useCase: readWorkspaceFileMetadataWithVersion, + present: async ({ file, share, currentVersion }) => ({ + data: { ...(await toV2File(file)), share, currentVersion }, + }), }) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/content/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/content/route.ts new file mode 100644 index 00000000000..d2f978f1572 --- /dev/null +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/content/route.ts @@ -0,0 +1,38 @@ +import { v2DownloadFileVersionContract } from '@/lib/api/contracts/v2/file-versions' +import { defineV2BinaryRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2FileErrorPolicies } from '@/lib/workspace-files/api' +import { downloadWorkspaceFileVersion } from '@/lib/workspace-files/application/file-versions' +import { fileOperations } from '@/lib/workspace-files/application/operations' +import { encodeFilenameForHeader } from '@/app/api/files/utils' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** + * GET /api/v2/files/[fileId]/versions/[version]/content — Download one version's bytes. + * + * Served exactly as `GET /api/v2/files/[fileId]` serves the current bytes: ordinary files stream, + * and a generated document resolves to its rendered artifact (`CONFLICT` while it compiles). + * + * `headSafe: false` because downloading records a `FILE_DOWNLOADED` audit event. + */ +export const GET = defineV2BinaryRoute({ + contract: v2DownloadFileVersionContract, + auth: v2ApiKeyAuth, + headSafe: false, + operation: fileOperations.downloadVersion, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, query }) => ({ + fileId: params.fileId, + assertedWorkspaceId: query.workspaceId, + version: params.version, + }), + useCase: downloadWorkspaceFileVersion, + present: ({ file, stream, contentType, contentLength }) => ({ + body: stream, + contentType, + contentDisposition: `attachment; ${encodeFilenameForHeader(file.name)}`, + contentLength, + }), +}) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.ts new file mode 100644 index 00000000000..81100c681b0 --- /dev/null +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/revert/route.ts @@ -0,0 +1,34 @@ +import { v2RevertFileVersionContract } from '@/lib/api/contracts/v2/file-versions' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2FileErrorPolicies } from '@/lib/workspace-files/api' +import { revertWorkspaceFileVersion } from '@/lib/workspace-files/application/file-versions' +import { fileOperations } from '@/lib/workspace-files/application/operations' +import { toV2File, toV2FileVersion } from '@/app/api/v2/files/utils' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** + * POST /api/v2/files/[fileId]/versions/[version]/revert — Make a version's content current again. + * + * Writes the version's bytes as a new version, so the revert can itself be reverted. Reverting to + * the current version is a no-op that reports `reverted: false`. + */ +export const POST = defineV2JsonRoute({ + contract: v2RevertFileVersionContract, + auth: v2ApiKeyAuth, + operation: fileOperations.revertVersion, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, body }) => ({ + fileId: params.fileId, + assertedWorkspaceId: body.workspaceId, + version: params.version, + expectedCurrentVersion: body.expectedCurrentVersion, + }), + useCase: revertWorkspaceFileVersion, + present: async ({ file, version, reverted }) => { + const [v2File, v2Version] = await Promise.all([toV2File(file), toV2FileVersion(version)]) + return { data: { reverted, file: v2File, version: v2Version } } + }, +}) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/route.ts new file mode 100644 index 00000000000..be7b535ae24 --- /dev/null +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/route.ts @@ -0,0 +1,47 @@ +import { + v2DeleteFileVersionContract, + v2GetFileVersionContract, +} from '@/lib/api/contracts/v2/file-versions' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2FileErrorPolicies } from '@/lib/workspace-files/api' +import { + deleteWorkspaceFileVersion, + readWorkspaceFileVersion, +} from '@/lib/workspace-files/application/file-versions' +import { fileOperations } from '@/lib/workspace-files/application/operations' +import { toV2FileVersion } from '@/app/api/v2/files/utils' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** GET /api/v2/files/[fileId]/versions/[version] — Read one version's metadata. */ +export const GET = defineV2JsonRoute({ + contract: v2GetFileVersionContract, + auth: v2ApiKeyAuth, + operation: fileOperations.readVersion, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, query }) => ({ + fileId: params.fileId, + assertedWorkspaceId: query.workspaceId, + version: params.version, + }), + useCase: readWorkspaceFileVersion, + present: async ({ version }) => ({ data: await toV2FileVersion(version) }), +}) + +/** DELETE /api/v2/files/[fileId]/versions/[version] — Permanently delete a superseded version. */ +export const DELETE = defineV2JsonRoute({ + contract: v2DeleteFileVersionContract, + auth: v2ApiKeyAuth, + operation: fileOperations.deleteVersion, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, query }) => ({ + fileId: params.fileId, + assertedWorkspaceId: query.workspaceId, + version: params.version, + }), + useCase: deleteWorkspaceFileVersion, + present: ({ file, version }) => ({ data: { fileId: file.id, version, deleted: true as const } }), +}) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts new file mode 100644 index 00000000000..e7fc4d54b1a --- /dev/null +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts @@ -0,0 +1,54 @@ +import { v2ReadFileVersionTextContract } from '@/lib/api/contracts/v2/file-versions' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2FileErrorPolicies } from '@/lib/workspace-files/api' +import { readWorkspaceFileVersionText } from '@/lib/workspace-files/application/file-versions' +import { fileOperations } from '@/lib/workspace-files/application/operations' + +export const dynamic = 'force-dynamic' + +/** + * GET /api/v2/files/[fileId]/versions/[version]/text — extract one version's text. + * + * Extracts exactly as `GET /api/v2/files/[fileId]/text` does, including rendering a generated + * document before parsing it. Head-safe for the same reason: nothing is audited or written. + */ +export const GET = defineV2JsonRoute({ + contract: v2ReadFileVersionTextContract, + auth: v2ApiKeyAuth, + operation: fileOperations.readVersionContent, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, query }) => ({ + fileId: params.fileId, + assertedWorkspaceId: query.workspaceId, + version: params.version, + maxBytes: query.maxBytes, + offset: query.offset, + limit: query.limit, + }), + useCase: readWorkspaceFileVersionText, + present: ({ + file, + version, + text, + truncated, + degraded, + degradedReason, + byteCount, + lineRange, + }) => ({ + data: { + fileId: file.id, + version: version.version, + name: file.name, + type: version.contentType, + text, + truncated, + degraded, + degradedReason, + charCount: text.length, + byteCount, + ...(lineRange ? { lineRange } : {}), + }, + }), +}) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/route.ts new file mode 100644 index 00000000000..999323890d2 --- /dev/null +++ b/apps/sim/app/api/v2/files/[fileId]/versions/route.ts @@ -0,0 +1,50 @@ +import { v2ListFileVersionsContract } from '@/lib/api/contracts/v2/file-versions' +import { cursorRoute, cursorScopeKey } from '@/lib/api/cursor-binding' +import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes' +import { v2FileErrorPolicies } from '@/lib/workspace-files/api' +import { listWorkspaceFileVersions } from '@/lib/workspace-files/application/file-versions' +import { fileOperations } from '@/lib/workspace-files/application/operations' +import { toV2FileVersions } from '@/app/api/v2/files/utils' +import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response' + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +/** + * Binds a cursor to the file whose history it pages. Version numbers restart at 1 per file, so an + * unbound cursor would resume another file's history at the same number. + */ +function versionCursorFilters(fileId: string) { + return cursorScopeKey(cursorRoute(v2ListFileVersionsContract, { fileId })) +} + +/** GET /api/v2/files/[fileId]/versions — List a file's versions, newest first by default. */ +export const GET = defineV2JsonRoute({ + contract: v2ListFileVersionsContract, + auth: v2ApiKeyAuth, + operation: fileOperations.listVersions, + rateLimit: v2RateLimits.publicApi, + errorPolicy: v2FileErrorPolicies.concealResourceAuthorization, + mapInput: ({ params, query }) => ({ + fileId: params.fileId, + assertedWorkspaceId: query.workspaceId, + sortOrder: query.sortOrder, + limit: query.limit, + after: readSortedCursor( + query.cursor, + query.sortBy, + query.sortOrder, + versionCursorFilters(params.fileId) + ), + }), + useCase: listWorkspaceFileVersions, + present: async ({ versions, nextKeys }, { params, query }) => ({ + data: await toV2FileVersions(versions), + nextCursor: writeSortedCursor( + nextKeys, + query.sortBy, + query.sortOrder, + versionCursorFilters(params.fileId) + ), + }), +}) diff --git a/apps/sim/app/api/v2/files/utils.ts b/apps/sim/app/api/v2/files/utils.ts index 66c1872f1dc..4f6222db6db 100644 --- a/apps/sim/app/api/v2/files/utils.ts +++ b/apps/sim/app/api/v2/files/utils.ts @@ -1,8 +1,10 @@ +import type { V2FileVersion } from '@/lib/api/contracts/v2/file-versions' import type { V2File } from '@/lib/api/contracts/v2/files' import { getBaseUrl } from '@/lib/core/utils/urls' import { buildFolderPath } from '@/lib/folders/paths' import { workspaceResourceWebUrl } from '@/lib/resources' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import type { WorkspaceFileVersionRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { getUserEmailsByIds, requireResolvedUserEmail } from '@/lib/users/queries' import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path' @@ -59,3 +61,35 @@ export async function toV2Files(records: WorkspaceFileRecord[]): Promise +): V2FileVersion { + return { + fileId: record.fileId, + version: record.version, + isCurrent: record.isCurrent, + size: record.size, + contentType: record.contentType, + source: record.source, + authors: record.authorUserIds.map((id) => ({ id, email: emailByUserId.get(id) ?? null })), + restoredFromVersion: record.restoredFromVersion, + createdAt: record.createdAt.toISOString(), + updatedAt: record.updatedAt.toISOString(), + supersededAt: record.supersededAt?.toISOString() ?? null, + } +} + +/** Serializes file versions, resolving every author's email in one query. */ +export async function toV2FileVersions( + records: WorkspaceFileVersionRecord[] +): Promise { + const emailByUserId = await getUserEmailsByIds(records.flatMap((record) => record.authorUserIds)) + return records.map((record) => serializeV2FileVersion(record, emailByUserId)) +} + +export async function toV2FileVersion(record: WorkspaceFileVersionRecord): Promise { + const [version] = await toV2FileVersions([record]) + return version +} diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts new file mode 100644 index 00000000000..434c124d22f --- /dev/null +++ b/apps/sim/background/cleanup-file-versions.ts @@ -0,0 +1,190 @@ +import { dbFor } from '@sim/db' +import { workspaceFileVersion } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { chunkArray } from '@sim/utils/helpers' +import { task } from '@trigger.dev/sdk' +import { and, count, gt, inArray, isNotNull, lt, min, or, sql } from 'drizzle-orm' +import type { CleanupJobPayload } from '@/lib/billing/cleanup-dispatcher' +import type { PlanCategory } from '@/lib/billing/plan-helpers' +import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' +import { retentionCleanupQueue } from '@/lib/cleanup/queue' +import { deleteWorkspaceStorageObjects } from '@/lib/cleanup/storage-delete' +import { isUsingCloudStorage } from '@/lib/uploads' +import { + FILE_VERSION_RETENTION_KEEP_LATEST, + MAX_SUPERSEDED_FILE_VERSIONS, +} from '@/lib/uploads/contexts/workspace/workspace-file-versions' + +const logger = createLogger('CleanupFileVersions') + +/** All cleanup queries run on the dedicated cleanup pool. */ +const cleanupDb = dbFor('cleanup') + +/** Workspaces whose candidate files are found in one query. */ +const WORKSPACES_PER_QUERY = 50 +/** Candidate files whose histories are ranked in one query. */ +const FILES_PER_QUERY = 500 +const VERSIONS_PER_BATCH = 1000 +/** Bounds one run's work per file chunk; the next daily run continues where this stopped. */ +const MAX_BATCHES_PER_CHUNK = 50 + +/** + * Most superseded versions a file keeps, per plan; any beyond it are pruned whatever their age. Free + * keeps its newest 100 versions (the current one included); every other plan is bounded only by the + * inline write-time ceiling. + */ +const MAX_SUPERSEDED_VERSIONS_BY_PLAN: Record = { + free: 99, + pro: MAX_SUPERSEDED_FILE_VERSIONS, + team: MAX_SUPERSEDED_FILE_VERSIONS, + enterprise: MAX_SUPERSEDED_FILE_VERSIONS, +} + +/** Newest superseded versions a file keeps whatever their age (the current version is the tenth). */ +const KEEP_SUPERSEDED = FILE_VERSION_RETENTION_KEEP_LATEST - 1 + +/** + * Files in the group that can lose any version: more superseded versions than the keep-latest floor, + * and either one older than the cutoff or more than the plan allows. Ranking only these keeps the + * sort to the histories that need pruning instead of every superseded row in the group. + */ +async function selectCandidateFileIds( + workspaceIds: string[], + cutoff: Date, + maxSuperseded: number +): Promise { + const rows = await cleanupDb + .select({ fileId: workspaceFileVersion.fileId }) + .from(workspaceFileVersion) + .where( + and( + inArray(workspaceFileVersion.workspaceId, workspaceIds), + isNotNull(workspaceFileVersion.supersededAt) + ) + ) + .groupBy(workspaceFileVersion.fileId) + .having( + and( + gt(count(), KEEP_SUPERSEDED), + or( + lt( + min(workspaceFileVersion.supersededAt), + sql.param(cutoff, workspaceFileVersion.supersededAt) + ), + gt(count(), maxSuperseded) + ) + ) + ) + return rows.map((row) => row.fileId) +} + +/** + * Superseded versions of the given files past retention: older than the cutoff or beyond the plan's + * count, but never among the newest {@link FILE_VERSION_RETENTION_KEEP_LATEST} versions of a file. + */ +function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: number) { + const ranked = cleanupDb + .select({ + id: workspaceFileVersion.id, + key: workspaceFileVersion.key, + supersededAt: workspaceFileVersion.supersededAt, + rank: sql`row_number() over (partition by ${workspaceFileVersion.fileId} order by ${workspaceFileVersion.version} desc)`.as( + 'rank' + ), + }) + .from(workspaceFileVersion) + .where( + and( + inArray(workspaceFileVersion.fileId, fileIds), + isNotNull(workspaceFileVersion.supersededAt) + ) + ) + .as('ranked') + + return cleanupDb + .select({ id: ranked.id, key: ranked.key }) + .from(ranked) + .where( + and( + gt(ranked.rank, KEEP_SUPERSEDED), + or(lt(ranked.supersededAt, cutoff), gt(ranked.rank, maxSuperseded)) + ) + ) + .limit(VERSIONS_PER_BATCH) +} + +/** + * Deletes the stored objects first and only then the rows whose objects are gone, so a failed + * delete leaves its row — and the next run retries it — instead of orphaning the object. Without + * cloud storage the objects are left in place, as the soft-delete purge leaves them. + */ +async function deleteVersions(rows: Array<{ id: string; key: string }>, label: string) { + const failedKeys = isUsingCloudStorage() + ? await deleteWorkspaceStorageObjects( + rows.map((row) => row.key), + label + ) + : new Set() + const removable = rows.filter((row) => !failedKeys.has(row.key)) + let deleted = 0 + for (const batch of chunkArray(removable, DEFAULT_DELETE_CHUNK_SIZE)) { + const removed = await cleanupDb + .delete(workspaceFileVersion) + .where( + and( + inArray( + workspaceFileVersion.id, + batch.map((row) => row.id) + ), + isNotNull(workspaceFileVersion.supersededAt) + ) + ) + .returning({ id: workspaceFileVersion.id }) + deleted += removed.length + } + return { deleted, failed: rows.length - removable.length } +} + +export async function runCleanupFileVersions(payload: CleanupJobPayload): Promise { + const startTime = Date.now() + const { workspaceIds, retentionHours, label, plan } = payload + if (workspaceIds.length === 0) { + logger.info(`[${label}] No workspaces to process`) + return + } + + const cutoff = new Date(Date.now() - retentionHours * 60 * 60 * 1000) + const maxSuperseded = MAX_SUPERSEDED_VERSIONS_BY_PLAN[plan] + logger.info( + `[${label}] Processing ${workspaceIds.length} workspaces, cutoff: ${cutoff.toISOString()}` + ) + + let deleted = 0 + let failed = 0 + for (const group of chunkArray(workspaceIds, WORKSPACES_PER_QUERY)) { + const candidates = await selectCandidateFileIds(group, cutoff, maxSuperseded) + for (const fileIds of chunkArray(candidates, FILES_PER_QUERY)) { + for (let batch = 0; batch < MAX_BATCHES_PER_CHUNK; batch++) { + const expired = await selectExpiredVersions(fileIds, cutoff, maxSuperseded) + if (expired.length === 0) break + const result = await deleteVersions(expired, label) + deleted += result.deleted + failed += result.failed + if (expired.length < VERSIONS_PER_BATCH || result.deleted === 0) break + } + } + } + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + logger.info( + `[${label}] File version cleanup: ${deleted} deleted, ${failed} failed in ${elapsed}s` + ) +} + +export const cleanupFileVersionsTask = task({ + id: 'cleanup-file-versions', + machine: 'large-1x', + queue: retentionCleanupQueue, + retry: { maxAttempts: 1 }, + run: runCleanupFileVersions, +}) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 5497948fd26..b358503c956 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -11,6 +11,7 @@ import { workflowMcpServer, workspaceFile, workspaceFiles, + workspaceFileVersion, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { chunkArray } from '@sim/utils/helpers' @@ -39,6 +40,7 @@ import { cleanupOwnerCondition, resolveCleanupOwnerScope, } from '@/lib/cleanup/resource-scope' +import { deleteWorkspaceStorageObjects } from '@/lib/cleanup/storage-delete' import { deduplicateFolderName } from '@/lib/folders/naming' import { hardDeleteDocuments } from '@/lib/knowledge/documents/service' import type { StorageContext } from '@/lib/uploads' @@ -222,6 +224,49 @@ async function cleanupWorkspaceFileStorage( return result } +/** Files whose version rows are read in one query while their objects are collected. */ +const VERSION_OBJECT_FILE_CHUNK_SIZE = 100 + +/** + * Deletes the stored objects of every earlier version of the workspace files about to be purged, + * before their version rows cascade away with the file. A file whose version objects could not all + * be deleted is withheld from this purge, so its rows survive for the next run instead of leaving + * those objects unreferenced. + */ +async function cleanupWorkspaceFileVersionStorage( + rows: WorkspaceFileScope['multiContextRows'], + label: string +): Promise<{ rows: WorkspaceFileScope['multiContextRows']; failed: number }> { + const workspaceRows = rows.filter((row) => row.context === 'workspace') + if (workspaceRows.length === 0 || !isUsingCloudStorage()) return { rows, failed: 0 } + + const currentKeyByFileId = new Map(workspaceRows.map((row) => [row.id, row.key])) + const withheldFileIds = new Set() + for (const fileIds of chunkArray( + [...currentKeyByFileId.keys()], + VERSION_OBJECT_FILE_CHUNK_SIZE + )) { + const versions = await cleanupDb + .select({ fileId: workspaceFileVersion.fileId, key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where(inArray(workspaceFileVersion.fileId, fileIds)) + const fileIdByKey = new Map( + versions + .filter((version) => version.key !== currentKeyByFileId.get(version.fileId)) + .map((version) => [version.key, version.fileId]) + ) + for (const key of await deleteWorkspaceStorageObjects([...fileIdByKey.keys()], label)) { + const fileId = fileIdByKey.get(key) + if (fileId) withheldFileIds.add(fileId) + } + } + + return { + rows: rows.filter((row) => !withheldFileIds.has(row.id)), + failed: withheldFileIds.size, + } +} + async function deleteExpiredLegacyWorkspaceFileRows( rows: WorkspaceFileScope['legacyRows'], retentionDate: Date, @@ -899,7 +944,12 @@ export async function runCleanupSoftDeletes( chatCleanup = await prepareChatCleanup([...doomedChatIds], label) } - const fileCleanup = await cleanupWorkspaceFileStorage(fileScope) + const versionCleanup = await cleanupWorkspaceFileVersionStorage(fileScope.multiContextRows, label) + if (budgets && versionCleanup.failed) throw new Error('File version storage cleanup failed') + const fileCleanup = await cleanupWorkspaceFileStorage({ + ...fileScope, + multiContextRows: versionCleanup.rows, + }) if (budgets && fileCleanup.filesFailed) throw new Error('File storage cleanup failed') let totalDeleted = 0 diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index 985015f27e6..f6ce7420e81 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -151,19 +151,29 @@ function overrideValueToHours(value: string): number | null | undefined { return toRetentionHours(value) } -function buildRetentionOverride(workspaceId: string, draft: PolicyDraft): RetentionOverride | null { - const override: RetentionOverride = { workspaceId } - const log = overrideValueToHours(draft.logDays) - const soft = overrideValueToHours(draft.softDeleteDays) - const task = overrideValueToHours(draft.taskCleanupDays) - if (log !== undefined) override.logRetentionHours = log - if (soft !== undefined) override.softDeleteRetentionHours = soft - if (task !== undefined) override.taskCleanupHours = task - const hasField = - override.logRetentionHours !== undefined || - override.softDeleteRetentionHours !== undefined || - override.taskCleanupHours !== undefined - return hasField ? override : null +/** + * Builds a workspace override from the editable fields, starting from the override being replaced so + * fields this form does not edit (such as the API-only `fileVersionRetentionHours`) survive a save. + */ +function buildRetentionOverride( + workspaceId: string, + draft: PolicyDraft, + existing: RetentionOverride | undefined +): RetentionOverride | null { + const override: RetentionOverride = { ...existing, workspaceId } + const edited = { + logRetentionHours: overrideValueToHours(draft.logDays), + softDeleteRetentionHours: overrideValueToHours(draft.softDeleteDays), + taskCleanupHours: overrideValueToHours(draft.taskCleanupDays), + } + for (const [field, hours] of Object.entries(edited) as [ + keyof typeof edited, + number | null | undefined, + ][]) { + if (hours === undefined) delete override[field] + else override[field] = hours + } + return Object.keys(override).length > 1 ? override : null } /** Stable serialization of a stage set for dirty-detection. */ @@ -878,8 +888,17 @@ function DataRetentionForm({ initialData: data, orgId, workspaces }: DataRetenti const clearIds = new Set([...editing.original.workspaceIds, ...ids]) const nextOverrides = overrides.filter((o) => !clearIds.has(o.workspaceId)) const nextPiiOverrides = piiOverrides.filter((p) => !clearIds.has(p.workspaceId)) + /** An edited override moved to another workspace keeps what only the API can set. */ + const replacedOverride = + editing.original.workspaceIds.length === 1 + ? overrides.find((o) => o.workspaceId === editing.original.workspaceIds[0]) + : undefined for (const workspaceId of ids) { - const ov = buildRetentionOverride(workspaceId, draft) + const ov = buildRetentionOverride( + workspaceId, + draft, + overrides.find((o) => o.workspaceId === workspaceId) ?? replacedOverride + ) if (ov) nextOverrides.push(ov) if (draft.piiOverride) { const existing = piiOverrides.find((p) => p.workspaceId === workspaceId) diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 304f9761e72..c451938dd4e 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -200,8 +200,9 @@ export function useWorkspaceImageDimensionsAdapter( /** * A read that addressed a storage object the file no longer points at. * - * A workspace file's bytes are rewritten under a NEW storage key on every content update and the - * superseded object is deleted (`updateWorkspaceFileContent`), so a 404 from a content read means + * A workspace file's bytes are rewritten under a NEW storage key on every content update, and the + * serve route answers only for the key the file currently points at (a superseded object is either + * deleted or kept as a version reachable through the version API), so a 404 from a content read means * "the key you are holding has been replaced", not "the server is broken" — the serve route says as * much and logs it at `info`. Distinguished from a transport failure so {@link useStaleKeyRecovery} * can re-resolve the record instead of surfacing a dead end. diff --git a/apps/sim/lib/api/contracts/deployments.ts b/apps/sim/lib/api/contracts/deployments.ts index b8bc3f94713..df68e798b97 100644 --- a/apps/sim/lib/api/contracts/deployments.ts +++ b/apps/sim/lib/api/contracts/deployments.ts @@ -1,4 +1,9 @@ import { z } from 'zod' +import { + INT4_MAX, + versionNumberPathSchema, + versionNumberSchema, +} from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' import { workflowIdParamsSchema } from '@/lib/api/contracts/workflows' import { @@ -27,36 +32,19 @@ export const deployedWorkflowStateSchema = z * comparison instead of missing, so every schema carrying a deployment version * — path param, request body, or cursor payload — must be bounded by this. */ -export const DEPLOYMENT_VERSION_MAX = 2147483647 +export const DEPLOYMENT_VERSION_MAX = INT4_MAX /** A deployment version number, bounded to the range its column can hold. */ -export const deploymentVersionNumberSchema = z - .number() - .int('version must be an integer') - .min(1, 'version must be a positive integer') - .max(DEPLOYMENT_VERSION_MAX, 'version is out of range') - -/** - * {@link deploymentVersionNumberSchema} for a path segment, which arrives as a - * string. Spelled out rather than piped through the body schema because a - * `ZodPipe` publishes none of its constraints to the generated OpenAPI document, - * which would leave the documented parameter unbounded even though the runtime - * check holds. - */ -const deploymentVersionPathSchema = z.coerce - .number() - .int() - .positive() - .max(DEPLOYMENT_VERSION_MAX, 'version is out of range') +export const deploymentVersionNumberSchema = versionNumberSchema export const deploymentVersionParamsSchema = z.object({ id: z.string().min(1, 'Invalid workflow ID'), - version: deploymentVersionPathSchema, + version: versionNumberPathSchema, }) export const deploymentVersionOrActiveParamsSchema = z.object({ id: z.string().min(1, 'Invalid workflow ID'), - version: z.union([deploymentVersionPathSchema, z.literal('active')]), + version: z.union([versionNumberPathSchema, z.literal('active')]), }) export const updatePublicApiBodySchema = z.object({ diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index cc395c4e5fc..35ad7fb972a 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -73,6 +73,7 @@ export const updateOrganizationDataRetentionBodySchema = z.object({ logRetentionHours: organizationDataRetentionHoursSchema, softDeleteRetentionHours: organizationDataRetentionHoursSchema, taskCleanupHours: organizationDataRetentionHoursSchema, + fileVersionRetentionHours: organizationDataRetentionHoursSchema, piiRedaction: piiRedactionSettingsSchema.optional(), retentionOverrides: retentionOverridesSchema.optional(), }) @@ -85,6 +86,7 @@ const organizationRetentionValuesSchema = z.object({ logRetentionHours: z.number().int().nullable(), softDeleteRetentionHours: z.number().int().nullable(), taskCleanupHours: z.number().int().nullable(), + fileVersionRetentionHours: z.number().int().nullable(), piiRedaction: piiRedactionSettingsSchema.nullable(), retentionOverrides: retentionOverridesSchema.nullable(), }) diff --git a/apps/sim/lib/api/contracts/primitives.ts b/apps/sim/lib/api/contracts/primitives.ts index 691dca98f86..c60f6c16be1 100644 --- a/apps/sim/lib/api/contracts/primitives.ts +++ b/apps/sim/lib/api/contracts/primitives.ts @@ -335,6 +335,28 @@ export const workspaceFileIdSchema = requiredFieldSchema('File ID is required') .max(MAX_ID_LENGTH, 'File ID is too long') .regex(/^[A-Za-z0-9_-]+$/, 'Invalid file id') +/** Upper bound of a Postgres `integer` column, the type every version number is stored as. */ +export const INT4_MAX = 2147483647 + +/** A version number in a body or cursor, bounded to the range its column can hold. */ +export const versionNumberSchema = z + .number() + .int('version must be an integer') + .min(1, 'version must be a positive integer') + .max(INT4_MAX, 'version is out of range') + +/** + * A version number arriving as a path segment. Coerced and bounded here rather than piped through + * a body schema, because a `ZodPipe` publishes none of its constraints to the generated OpenAPI + * document, which would leave the documented parameter unbounded even though the runtime check + * holds. + */ +export const versionNumberPathSchema = z.coerce + .number() + .int() + .positive() + .max(INT4_MAX, 'version is out of range') + /** * Reference to an image embedded in a document: either a workspace storage `key` * (serve-URL embeds) or a workspace file `id` (view-URL embeds) — exactly one. Shared @@ -533,6 +555,7 @@ export const retentionOverrideSchema = z.object({ logRetentionHours: retentionOverrideHoursSchema, softDeleteRetentionHours: retentionOverrideHoursSchema, taskCleanupHours: retentionOverrideHoursSchema, + fileVersionRetentionHours: retentionOverrideHoursSchema, }) export type RetentionOverride = z.output diff --git a/apps/sim/lib/api/contracts/v2/__tests__/cross-cutting.test.ts b/apps/sim/lib/api/contracts/v2/__tests__/cross-cutting.test.ts index 4c432cca258..cd1c9381067 100644 --- a/apps/sim/lib/api/contracts/v2/__tests__/cross-cutting.test.ts +++ b/apps/sim/lib/api/contracts/v2/__tests__/cross-cutting.test.ts @@ -181,11 +181,12 @@ describe('knowledge and files request-slice strictness', () => { * download, and permanent delete. It falls when two routes become one: 106 → * 105 when the archived knowledge-base list folded into `GET /knowledge` as * `scope=archived`, 105 → 108 with the file content-search query and the - * in-place content edit's query and body, and 108 → 109 with the knowledge - * base export query. + * in-place content edit's query and body, 108 → 109 with the knowledge + * base export query, and 109 → 116 with the file version list, get, text, + * download, revert, and delete queries and the revert body. */ it('sweeps every documented query and body slice', () => { - expect(slices.length).toBe(109) + expect(slices.length).toBe(116) }) it.each(slices)('%s rejects an undeclared key', (_name, schema) => { diff --git a/apps/sim/lib/api/contracts/v2/__tests__/list-pagination.test.ts b/apps/sim/lib/api/contracts/v2/__tests__/list-pagination.test.ts index bf27fc9d660..f8bcec04ae0 100644 --- a/apps/sim/lib/api/contracts/v2/__tests__/list-pagination.test.ts +++ b/apps/sim/lib/api/contracts/v2/__tests__/list-pagination.test.ts @@ -52,6 +52,7 @@ const PAGED_LISTS = [ 'GET /api/v2/credentials', 'GET /api/v2/custom-tools', 'GET /api/v2/files', + 'GET /api/v2/files/[fileId]/versions', 'GET /api/v2/knowledge', 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors', 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors/[connectorId]/documents', @@ -178,6 +179,7 @@ const CURSOR_BINDINGS: Record = { /** Decides whether `folderPath` covers one folder or its whole subtree. */ 'recursive', ], + 'GET /api/v2/files/[fileId]/versions': ['sortBy', 'sortOrder'], 'GET /api/v2/knowledge': ['workspaceId', 'scope', 'folderPath', 'search', 'sortBy', 'sortOrder'], 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors': ['workspaceId', 'sortBy', 'sortOrder'], 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors/[connectorId]/documents': [ @@ -287,6 +289,7 @@ const CURSOR_BINDINGS: Record = { * resolves the path before fingerprinting it. */ const CURSOR_BOUND_PATH_PARAMS: Record = { + 'GET /api/v2/files/[fileId]/versions': ['fileId'], 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors': ['knowledgeBaseId'], 'GET /api/v2/knowledge/[knowledgeBaseId]/connectors/[connectorId]/documents': [ 'knowledgeBaseId', @@ -320,6 +323,10 @@ const CURSOR_BOUND_PATH_PARAMS: Record = { * correctness gain. */ const UNBOUND_PARAMS: Record> = { + 'GET /api/v2/files/[fileId]/versions': { + workspaceId: + 'Asserted scope, not a filter: the sequence is one file, named by the path. A mismatched workspace is refused by authorization before paging.', + }, 'GET /api/v2/audit-logs': { organizationId: 'Asserted scope, not a filter: an account belongs to at most one organization, so naming it and omitting it select the same sequence. The resolved id is decided inside the application use case, so the route cannot stamp it without resolving an authorization decision itself.', diff --git a/apps/sim/lib/api/contracts/v2/file-versions.ts b/apps/sim/lib/api/contracts/v2/file-versions.ts new file mode 100644 index 00000000000..b5719092849 --- /dev/null +++ b/apps/sim/lib/api/contracts/v2/file-versions.ts @@ -0,0 +1,251 @@ +import { z } from 'zod' +import { + noInputSchema, + versionNumberPathSchema, + versionNumberSchema, + workspaceIdSchema, +} from '@/lib/api/contracts/primitives' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { + v2FileParamsSchema, + v2FileSchema, + v2FileTextSchema, + v2FileWorkspaceQuerySchema, + v2ReadFileTextQuerySchema, +} from '@/lib/api/contracts/v2/files' +import { + v2CursorListResponse, + v2DataResponse, + v2PaginationFields, + v2SortFields, +} from '@/lib/api/contracts/v2/shared' + +/** + * v2 file version history contracts. Every content write — an upload, an API or editor save, a + * Sim edit, a workflow write, a revert — records a version; collaborative edits and repeated + * workflow writes from one author fold into one version per ten-minute window. Renames and moves + * are metadata changes and never create versions, so every version reads under the file's + * current name. + */ + +export const v2FileVersionSourceSchema = z + .enum(['upload', 'user', 'api', 'copilot', 'workflow', 'collab', 'revert', 'unknown']) + .describe( + 'What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed).' + ) + +export const v2FileVersionAuthorSchema = z + .object({ + id: z.string().describe('User identifier.'), + email: z + .email({ pattern: z.regexes.html5Email }) + .nullable() + .describe('Current email address of the user, or null when the account no longer exists.'), + }) + .strict() + +/** One version of a workspace file as exposed by the v2 surface. */ +export const v2FileVersionSchema = z + .object({ + fileId: z.string().describe('File this version belongs to.'), + version: z + .number() + .int() + .positive() + .describe( + 'Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.' + ) + .meta({ examples: [3] }), + isCurrent: z.boolean().describe('Whether this version holds the current content of the file.'), + size: z.number().nonnegative().describe('Size in bytes of the stored content of this version.'), + contentType: z.string().describe('MIME type of the stored content of this version.'), + source: v2FileVersionSourceSchema, + authors: z + .array(v2FileVersionAuthorSchema) + .describe( + 'Users who wrote this version, in order of first contribution. Empty for actorless writers such as workspace API keys. A collaborative version lists every editor in its window.' + ), + restoredFromVersion: z + .number() + .int() + .positive() + .nullable() + .describe('For a `revert` version, the version whose content it restored; otherwise null.'), + createdAt: z + .string() + .describe('ISO 8601 timestamp when this content became current.') + .meta({ format: 'date-time', examples: ['2026-01-15T10:30:00Z'] }), + updatedAt: z + .string() + .describe( + 'ISO 8601 timestamp of the last write folded into this version. Equals `createdAt` unless edits were coalesced into it.' + ) + .meta({ format: 'date-time', examples: ['2026-01-15T10:38:00Z'] }), + supersededAt: z + .string() + .nullable() + .describe( + 'ISO 8601 timestamp when a newer version replaced this one, or null for the current version. Retention ages versions from this time.' + ) + .meta({ format: 'date-time', examples: ['2026-01-16T09:00:00Z'] }), + }) + .strict() + .meta({ + id: 'V2FileVersion', + title: 'File version', + description: 'One recorded version of the content of a workspace file.', + }) + +export type V2FileVersion = z.output + +export const v2FileVersionParamsSchema = v2FileParamsSchema.extend({ + version: versionNumberPathSchema.describe('Version number.'), +}) + +export const v2FileVersionSortFields = ['version'] as const + +export const v2ListFileVersionsQuerySchema = z + .object({ + workspaceId: workspaceIdSchema.describe('Workspace that owns the file.'), + ...v2SortFields(v2FileVersionSortFields, { sortBy: 'version', sortOrder: 'desc' }), + ...v2PaginationFields({ description: 'Maximum versions to return per page.' }), + }) + .strict() + +export type V2ListFileVersionsQuery = z.output + +export const v2FileVersionTextSchema = v2FileTextSchema + .extend({ + version: z.number().int().positive().describe('Version the text was extracted from.'), + }) + .meta({ + id: 'V2FileVersionText', + title: 'Extracted file version text', + description: + 'Text extracted from one version of a workspace file, with extraction-quality flags.', + }) + +export const v2RevertFileVersionBodySchema = z + .object({ + workspaceId: workspaceIdSchema.describe('Workspace that owns the file.'), + expectedCurrentVersion: versionNumberSchema + .optional() + .describe( + 'Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number.' + ), + }) + .strict() + +export type V2RevertFileVersionBody = z.input + +export const v2RevertFileVersionResultSchema = z + .object({ + reverted: z + .boolean() + .describe( + 'False when the requested version was already current, in which case nothing was written.' + ), + file: v2FileSchema, + version: v2FileVersionSchema.describe( + 'The current version of the file after the revert: a new `revert` version; the requested version when it was already current; or the unchanged current version when its content already matched the requested one.' + ), + }) + .strict() + .meta({ + id: 'V2FileVersionRevertResult', + title: 'Revert file version result', + description: 'The file and its current version after a revert.', + }) + +export type V2FileVersionRevertResult = z.output + +export const v2ListFileVersionsContract = defineRouteContract({ + method: 'GET', + path: '/api/v2/files/[fileId]/versions', + params: v2FileParamsSchema, + query: v2ListFileVersionsQuerySchema, + response: { + mode: 'json', + schema: v2CursorListResponse(v2FileVersionSchema), + }, +}) + +export const v2GetFileVersionContract = defineRouteContract({ + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]', + params: v2FileVersionParamsSchema, + query: v2FileWorkspaceQuerySchema, + response: { + mode: 'json', + schema: v2DataResponse(v2FileVersionSchema), + }, +}) + +/** Text of one version, extracted exactly as `GET /api/v2/files/[fileId]/text` extracts it. */ +export const v2ReadFileVersionTextContract = defineRouteContract({ + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]/text', + params: v2FileVersionParamsSchema, + query: v2ReadFileTextQuerySchema, + response: { + mode: 'json', + schema: v2DataResponse(v2FileVersionTextSchema), + }, +}) + +/** Bytes of one version, served exactly as `GET /api/v2/files/[fileId]` serves the current ones. */ +export const v2DownloadFileVersionContract = defineRouteContract({ + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]/content', + params: v2FileVersionParamsSchema, + query: v2FileWorkspaceQuerySchema, + response: { + mode: 'binary', + }, +}) + +export const v2DeleteFileVersionResultSchema = z + .object({ + fileId: z.string().describe('File whose version was deleted.'), + version: z.number().int().positive().describe('Version number that was deleted.'), + deleted: z.literal(true).describe('Always true: the version and its stored content are gone.'), + }) + .strict() + .meta({ + id: 'V2FileVersionDeleteResult', + title: 'Delete file version result', + description: 'Deletion acknowledgement for one file version.', + }) + +export type V2FileVersionDeleteResult = z.output + +/** + * Permanently deletes one superseded version, for purging content from history before retention + * would. The current version is the file itself and cannot be deleted. + */ +export const v2DeleteFileVersionContract = defineRouteContract({ + method: 'DELETE', + path: '/api/v2/files/[fileId]/versions/[version]', + params: v2FileVersionParamsSchema, + query: v2FileWorkspaceQuerySchema, + response: { + mode: 'json', + schema: v2DataResponse(v2DeleteFileVersionResultSchema), + }, +}) + +/** + * Makes a version's content current again by writing it as a new version, so a revert is itself + * revertible. Named `revert` because `restore` already means un-archiving a deleted file. + */ +export const v2RevertFileVersionContract = defineRouteContract({ + method: 'POST', + path: '/api/v2/files/[fileId]/versions/[version]/revert', + params: v2FileVersionParamsSchema, + query: noInputSchema, + body: v2RevertFileVersionBodySchema, + response: { + mode: 'json', + schema: v2DataResponse(v2RevertFileVersionResultSchema), + }, +}) diff --git a/apps/sim/lib/api/contracts/v2/files.ts b/apps/sim/lib/api/contracts/v2/files.ts index 644c51557f1..389a05705a2 100644 --- a/apps/sim/lib/api/contracts/v2/files.ts +++ b/apps/sim/lib/api/contracts/v2/files.ts @@ -155,6 +155,13 @@ export const v2FileMetadataSchema = v2FileSchema share: v2FileShareSchema .nullable() .describe('Current public-share state, or null when the file has never been shared.'), + currentVersion: z + .number() + .int() + .positive() + .describe( + 'Version number of the current content. List File Versions returns the history; pass this as `expectedCurrentVersion` to revert only if nothing changed since.' + ), }) .meta({ id: 'V2FileMetadata', diff --git a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts index 3515fa38708..7dd1f824d84 100644 --- a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts +++ b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts @@ -1,4 +1,12 @@ import { v2GetAuditLogContract, v2ListAuditLogsContract } from '@/lib/api/contracts/v2/audit-logs' +import { + v2DeleteFileVersionContract, + v2DownloadFileVersionContract, + v2GetFileVersionContract, + v2ListFileVersionsContract, + v2ReadFileVersionTextContract, + v2RevertFileVersionContract, +} from '@/lib/api/contracts/v2/file-versions' import { v2AbortFileUploadContract, v2BulkDeleteFilesContract, @@ -73,6 +81,20 @@ const FILE_EXAMPLE = { deletedAt: null, } as const +const FILE_VERSION_EXAMPLE = { + fileId: FILE_EXAMPLE.id, + version: 3, + isCurrent: true, + size: 1024, + contentType: 'text/csv', + source: 'api', + authors: [{ id: 'usr_4kJ9mN2pQ7rS', email: 'jane@example.com' }], + restoredFromVersion: null, + createdAt: '2026-01-15T10:30:00Z', + updatedAt: '2026-01-15T10:30:00Z', + supersededAt: null, +} as const + const SHARE_EXAMPLE = { id: 'shr_8Hf3kL9wQ2mNpXr6Tz1Vb', token: 'share-token-example', @@ -418,6 +440,216 @@ const declaredRoutes = [ ), } ), + defineOpenApiRoute( + v2ListFileVersionsContract, + filesOperation({ + applicationOperation: fileOperations.listVersions, + operationId: 'listFileVersions', + summary: 'List File Versions', + description: + 'List the recorded versions of a file, newest first by default. Every content write records a version; collaborative edits and repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. Retention removes old versions by plan, always keeping the newest ten, so version numbers can have gaps.', + errors: RESOURCE_ERRORS, + success: { description: 'A page of file versions.' }, + }), + { + params: documentedSchema( + v2ListFileVersionsContract.params, + 'ListFileVersionsParams', + 'List file versions path parameters', + 'File whose versions are listed.' + ), + query: documentedSchema( + v2ListFileVersionsContract.query, + 'ListFileVersionsQuery', + 'List file versions query', + 'Workspace scope, sort direction, and pagination for a version list.' + ), + response: documentedSchema( + v2ListFileVersionsContract.response.schema, + 'V2FileVersionListResponse', + 'File version list response', + 'A cursor-paginated page of file versions.', + [{ data: [FILE_VERSION_EXAMPLE], nextCursor: null }] + ), + } + ), + defineOpenApiRoute( + v2GetFileVersionContract, + filesOperation({ + applicationOperation: fileOperations.readVersion, + operationId: 'getFileVersion', + summary: 'Get File Version', + description: + 'Get one version of a file. A version removed by retention, or one that never existed, returns `404`.', + errors: RESOURCE_ERRORS, + success: { description: 'The file version.' }, + }), + { + params: documentedSchema( + v2GetFileVersionContract.params, + 'FileVersionParams', + 'File version path parameters', + 'File and version selected by the request path.' + ), + query: documentedSchema( + v2GetFileVersionContract.query, + 'FileVersionQuery', + 'File version query', + 'Workspace scope for the file.' + ), + response: documentedSchema( + v2GetFileVersionContract.response.schema, + 'V2FileVersionResponse', + 'File version response', + 'A single file version.', + [{ data: FILE_VERSION_EXAMPLE }] + ), + } + ), + defineOpenApiRoute( + v2DeleteFileVersionContract, + filesOperation({ + applicationOperation: fileOperations.deleteVersion, + operationId: 'deleteFileVersion', + summary: 'Delete File Version', + description: + 'Permanently delete one earlier version and its stored content, for example to purge a leaked value from history before retention removes it. The current version returns `409`; revert to another version first. A version that does not exist returns `404`.', + errors: RESOURCE_CONFLICT_ERRORS, + success: { description: 'Deletion confirmation.' }, + }), + { + params: documentedSchema( + v2DeleteFileVersionContract.params, + 'FileVersionParams', + 'File version path parameters', + 'File and version selected by the request path.' + ), + query: documentedSchema( + v2DeleteFileVersionContract.query, + 'FileVersionQuery', + 'File version query', + 'Workspace scope for the file.' + ), + response: documentedSchema( + v2DeleteFileVersionContract.response.schema, + 'V2FileVersionDeleteResponse', + 'Delete file version response', + 'Deletion confirmation for one file version.', + [{ data: { fileId: FILE_EXAMPLE.id, version: 2, deleted: true } }] + ), + } + ), + defineOpenApiRoute( + v2ReadFileVersionTextContract, + filesOperation({ + applicationOperation: fileOperations.readVersionContent, + operationId: 'readFileVersionText', + summary: 'Read File Version Text', + description: + 'Extract the text of one version, exactly as Read File Text extracts the current content. Unsupported types return `400`, compiling documents return `409`, and oversized versions return `413`.', + errors: [...RESOURCE_CONFLICT_ERRORS, 'PayloadTooLarge'], + success: { + description: 'The extracted text of the version and its extraction-quality flags.', + }, + }), + { + params: documentedSchema( + v2ReadFileVersionTextContract.params, + 'FileVersionParams', + 'File version path parameters', + 'File and version selected by the request path.' + ), + query: documentedSchema( + v2ReadFileVersionTextContract.query, + 'ReadFileVersionTextQuery', + 'Read file version text query', + 'Workspace scope, optional source-byte ceiling, and optional line window.' + ), + response: documentedSchema( + v2ReadFileVersionTextContract.response.schema, + 'FileVersionTextResponse', + 'File version text response', + 'Text extracted from one version of a workspace file.' + ), + } + ), + defineOpenApiRoute( + v2DownloadFileVersionContract, + filesOperation({ + applicationOperation: fileOperations.downloadVersion, + operationId: 'downloadFileVersion', + summary: 'Download File Version', + description: `Download the bytes of one version, served exactly as Download File serves the current bytes. Generated documents use compiled artifacts, returning \`409\` while compiling and \`413\` above the rendered-size ceiling. Downloading records an audit event. ${HEAD_MIRRORS_GET} ${HEAD_OMITS_PAYLOAD_HEADERS}`, + errors: [...RESOURCE_CONFLICT_ERRORS, 'PayloadTooLarge'], + success: { + description: 'The version bytes.', + headers: ['Content-Type', 'Content-Disposition', 'Content-Length'], + contentTypes: ['application/octet-stream'], + }, + }), + { + params: documentedSchema( + v2DownloadFileVersionContract.params, + 'FileVersionParams', + 'File version path parameters', + 'File and version selected by the request path.' + ), + query: documentedSchema( + v2DownloadFileVersionContract.query, + 'FileVersionQuery', + 'File version query', + 'Workspace scope for the file.' + ), + } + ), + defineOpenApiRoute( + v2RevertFileVersionContract, + filesOperation({ + applicationOperation: fileOperations.revertVersion, + operationId: 'revertFileVersion', + summary: 'Revert File Version', + description: + 'Make the content of a version current again by writing it as a new `revert` version, so the revert can itself be reverted. Open editors receive the change. Reverting to the current version writes nothing and returns `reverted: false`. A concurrent write, or an `expectedCurrentVersion` that is no longer current, returns `409`; a version above 100 MB returns `413`.', + errors: [...RESOURCE_CONFLICT_ERRORS, 'PayloadTooLarge'], + success: { description: 'The file and its current version after the revert.' }, + }), + { + query: v2RevertFileVersionContract.query, + params: documentedSchema( + v2RevertFileVersionContract.params, + 'FileVersionParams', + 'File version path parameters', + 'File and version selected by the request path.' + ), + body: documentedSchema( + v2RevertFileVersionContract.body, + 'RevertFileVersionRequest', + 'Revert file version request', + 'Workspace scope and an optional current-version precondition.', + [{ workspaceId: 'a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64', expectedCurrentVersion: 4 }] + ), + response: documentedSchema( + v2RevertFileVersionContract.response.schema, + 'V2FileVersionRevertResponse', + 'Revert file version response', + 'The file and its current version after the revert.', + [ + { + data: { + reverted: true, + file: FILE_EXAMPLE, + version: { + ...FILE_VERSION_EXAMPLE, + version: 5, + source: 'revert', + restoredFromVersion: 3, + }, + }, + }, + ] + ), + } + ), defineOpenApiRoute( v2BulkDownloadFilesContract, filesOperation({ @@ -616,7 +848,7 @@ const declaredRoutes = [ operationId: 'getFile', summary: 'Get File Metadata', description: - 'Get file metadata and its public-share configuration. The `share` field is null when the file has never been shared.', + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.', errors: RESOURCE_ERRORS, success: { description: 'File metadata and public-share state.' }, }), @@ -639,8 +871,8 @@ const declaredRoutes = [ 'File metadata response', 'File metadata enriched with its current nullable public-share state.', [ - { data: { ...FILE_EXAMPLE, share: null } }, - { data: { ...FILE_EXAMPLE, share: SHARE_EXAMPLE } }, + { data: { ...FILE_EXAMPLE, share: null, currentVersion: 1 } }, + { data: { ...FILE_EXAMPLE, share: SHARE_EXAMPLE, currentVersion: 3 } }, ] ), } @@ -1156,7 +1388,8 @@ export const filesAuditOpenApiDocument = defineOpenApiDocument({ tags: [ { name: 'Files', - description: 'Create, upload, download, organize, share, and delete workspace files.', + description: + 'Create, upload, download, organize, share, version, and delete workspace files.', }, { name: 'Audit Logs', diff --git a/apps/sim/lib/api/mcp/generated/v2-operations.ts b/apps/sim/lib/api/mcp/generated/v2-operations.ts index 37a8e4cd58a..3a27c3c3b64 100644 --- a/apps/sim/lib/api/mcp/generated/v2-operations.ts +++ b/apps/sim/lib/api/mcp/generated/v2-operations.ts @@ -41,6 +41,13 @@ import { v2ListCustomToolsContract, v2UpdateCustomToolContract, } from '@/lib/api/contracts/v2/custom-tools' +import { + v2DeleteFileVersionContract, + v2GetFileVersionContract, + v2ListFileVersionsContract, + v2ReadFileVersionTextContract, + v2RevertFileVersionContract, +} from '@/lib/api/contracts/v2/file-versions' import { v2AbortFileUploadContract, v2BulkDeleteFilesContract, @@ -732,6 +739,14 @@ export const V2_MCP_OPERATIONS = { 'Archive an empty folder, or set `recursive=true` to archive its files and subfolders. Use Restore Folder to recover the archived contents.\n\nOAuth scope: `api:write`.', handler: () => import('@/app/api/v2/files/folders/route').then((route) => route.DELETE), }, + deleteFileVersion: { + contract: v2DeleteFileVersionContract, + summary: 'Delete File Version', + description: + 'Permanently delete one earlier version and its stored content, for example to purge a leaked value from history before retention removes it. The current version returns `409`; revert to another version first. A version that does not exist returns `404`.\n\nOAuth scope: `api:write`.', + handler: () => + import('@/app/api/v2/files/[fileId]/versions/[version]/route').then((route) => route.DELETE), + }, deleteKnowledgeBase: { contract: v2DeleteKnowledgeBaseContract, summary: 'Delete Knowledge Base', @@ -1020,7 +1035,7 @@ export const V2_MCP_OPERATIONS = { contract: v2GetFileContract, summary: 'Get File Metadata', description: - 'Get file metadata and its public-share configuration. The `share` field is null when the file has never been shared.\n\nOAuth scope: `api:read`.', + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/[fileId]/metadata/route').then((route) => route.GET), }, getFileShare: { @@ -1037,6 +1052,14 @@ export const V2_MCP_OPERATIONS = { "Get an upload session's state to determine whether an interrupted transfer can resume. Requires the signed upload token and current workspace access.\n\nOAuth scope: `api:read`.", handler: () => import('@/app/api/v2/files/uploads/[uploadId]/route').then((route) => route.GET), }, + getFileVersion: { + contract: v2GetFileVersionContract, + summary: 'Get File Version', + description: + 'Get one version of a file. A version removed by retention, or one that never existed, returns `404`.\n\nOAuth scope: `api:read`.', + handler: () => + import('@/app/api/v2/files/[fileId]/versions/[version]/route').then((route) => route.GET), + }, getKnowledgeBase: { contract: v2GetKnowledgeBaseContract, summary: 'Get Knowledge Base', @@ -1398,6 +1421,13 @@ export const V2_MCP_OPERATIONS = { 'List active workspace files with folder filtering, search, sorting, and cursor pagination. Use `scope=archived` to find files available for restoration. Workspace folder trees exceeding 10,000 folders return `413`.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/route').then((route) => route.GET), }, + listFileVersions: { + contract: v2ListFileVersionsContract, + summary: 'List File Versions', + description: + 'List the recorded versions of a file, newest first by default. Every content write records a version; collaborative edits and repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. Retention removes old versions by plan, always keeping the newest ten, so version numbers can have gaps.\n\nOAuth scope: `api:read`.', + handler: () => import('@/app/api/v2/files/[fileId]/versions/route').then((route) => route.GET), + }, listKnowledgeBases: { contract: v2ListKnowledgeBasesContract, summary: 'List Knowledge Bases', @@ -1776,6 +1806,16 @@ export const V2_MCP_OPERATIONS = { 'Extract text without changing the file. Use Unzip File to unpack archives or Download File for original bytes. Unsupported types return `400`, compiling documents return `409`, and oversized files return `413`. `degraded: true` indicates incomplete or synthesized text, such as the legacy `.pptx` fallback; `truncated: true` indicates a parser limit.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/[fileId]/text/route').then((route) => route.GET), }, + readFileVersionText: { + contract: v2ReadFileVersionTextContract, + summary: 'Read File Version Text', + description: + 'Extract the text of one version, exactly as Read File Text extracts the current content. Unsupported types return `400`, compiling documents return `409`, and oversized versions return `413`.\n\nOAuth scope: `api:read`.', + handler: () => + import('@/app/api/v2/files/[fileId]/versions/[version]/text/route').then( + (route) => route.GET + ), + }, relocateFileFolder: { contract: v2RelocateFileFolderContract, summary: 'Rename or Move Folder', @@ -1886,6 +1926,16 @@ export const V2_MCP_OPERATIONS = { (route) => route.POST ), }, + revertFileVersion: { + contract: v2RevertFileVersionContract, + summary: 'Revert File Version', + description: + 'Make the content of a version current again by writing it as a new `revert` version, so the revert can itself be reverted. Open editors receive the change. Reverting to the current version writes nothing and returns `reverted: false`. A concurrent write, or an `expectedCurrentVersion` that is no longer current, returns `409`; a version above 100 MB returns `413`.\n\nOAuth scope: `api:write`.', + handler: () => + import('@/app/api/v2/files/[fileId]/versions/[version]/revert/route').then( + (route) => route.POST + ), + }, revertWorkflowVersion: { contract: v2RevertWorkflowVersionContract, summary: 'Revert Workflow To Version', diff --git a/apps/sim/lib/billing/cleanup-dispatcher.ts b/apps/sim/lib/billing/cleanup-dispatcher.ts index ea9551c3209..61b2ead88de 100644 --- a/apps/sim/lib/billing/cleanup-dispatcher.ts +++ b/apps/sim/lib/billing/cleanup-dispatcher.ts @@ -28,7 +28,11 @@ const WORKSPACE_SCOPE_PAGE_SIZE = 500 /** Bounds per-run memory + DB connections regardless of plan size. */ const WORKSPACES_PER_CLEANUP_CHUNK = 500 -export type CleanupJobType = 'cleanup-logs' | 'cleanup-soft-deletes' | 'cleanup-tasks' +export type CleanupJobType = + | 'cleanup-logs' + | 'cleanup-soft-deletes' + | 'cleanup-tasks' + | 'cleanup-file-versions' export type NonEnterprisePlan = Exclude @@ -85,6 +89,10 @@ export const CLEANUP_CONFIG = { key: 'taskCleanupHours', defaults: { free: null, pro: null, team: null, enterprise: null }, }, + 'cleanup-file-versions': { + key: 'fileVersionRetentionHours', + defaults: { free: 30 * DAY, pro: 180 * DAY, team: 180 * DAY, enterprise: null }, + }, } as const satisfies Record async function listActiveWorkspaceCleanupScopeRowsPage( @@ -224,6 +232,8 @@ async function buildCleanupRunner(jobType: CleanupJobType): Promise cleanupRunner(payload as CleanupJobPayload)) as EnqueueOptions['runner'] diff --git a/apps/sim/lib/billing/retention.ts b/apps/sim/lib/billing/retention.ts index 56bb7b466e0..74c0f2d7a2f 100644 --- a/apps/sim/lib/billing/retention.ts +++ b/apps/sim/lib/billing/retention.ts @@ -124,6 +124,7 @@ export type RetentionHoursKey = | 'logRetentionHours' | 'softDeleteRetentionHours' | 'taskCleanupHours' + | 'fileVersionRetentionHours' interface PiiRedactionRulesLike { rules?: Array<{ workspaceId?: string | null }> | null diff --git a/apps/sim/lib/cleanup/storage-delete.ts b/apps/sim/lib/cleanup/storage-delete.ts new file mode 100644 index 00000000000..af7003db26f --- /dev/null +++ b/apps/sim/lib/cleanup/storage-delete.ts @@ -0,0 +1,26 @@ +import { createLogger } from '@sim/logger' +import { chunkArray } from '@sim/utils/helpers' +import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' +import { StorageService } from '@/lib/uploads' + +const logger = createLogger('CleanupStorageDelete') + +/** + * Deletes workspace storage objects in provider-sized chunks and returns the keys that could not be + * deleted, so callers keep the rows that reference them for the next run instead of orphaning the + * objects. Missing objects count as deleted. + */ +export async function deleteWorkspaceStorageObjects( + keys: readonly string[], + label: string +): Promise> { + const failedKeys = new Set() + for (const batch of chunkArray([...keys], DEFAULT_DELETE_CHUNK_SIZE)) { + const deletion = await StorageService.deleteFiles(batch, 'workspace') + for (const { key, error } of deletion.failed) { + failedKeys.add(key) + logger.error(`[${label}] Failed to delete storage object ${key}`, { error }) + } + } + return failedKeys +} diff --git a/apps/sim/lib/collab-doc/persist.ts b/apps/sim/lib/collab-doc/persist.ts index 0551d4bec2e..eec7af8dc97 100644 --- a/apps/sim/lib/collab-doc/persist.ts +++ b/apps/sim/lib/collab-doc/persist.ts @@ -101,6 +101,7 @@ export async function persistFileDoc( markdown, undefined, { + version: { source: 'collab', authorUserId: userId }, syncLiveDoc: false, expectedUpdatedAt: new Date(version), secretProvenancePolicy: { mode: 'preserve' }, diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index a1e54bbd2fd..b0de23c96fb 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -185,6 +185,7 @@ const JOB_TYPE_TO_TASK_ID: Record = { 'cleanup-soft-deletes': 'cleanup-soft-deletes', 'cleanup-table-row-ttl': 'cleanup-table-row-ttl', 'cleanup-tasks': 'cleanup-tasks', + 'cleanup-file-versions': 'cleanup-file-versions', 'run-data-drain': 'run-data-drain', 'knowledge-connector-directory-sync': 'knowledge-connector-directory-sync', } diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index c24ce618618..1cbecd49de0 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -48,6 +48,7 @@ export type JobType = | 'cleanup-soft-deletes' | 'cleanup-table-row-ttl' | 'cleanup-tasks' + | 'cleanup-file-versions' | 'run-data-drain' | 'knowledge-connector-directory-sync' diff --git a/apps/sim/lib/internal/file/operations.test.ts b/apps/sim/lib/internal/file/operations.test.ts index b48ba50154c..fe0678a266f 100644 --- a/apps/sim/lib/internal/file/operations.test.ts +++ b/apps/sim/lib/internal/file/operations.test.ts @@ -1723,6 +1723,7 @@ describe('file manage operations', () => { Buffer.from('fresh'), 'text/plain', { + version: { source: 'workflow', authorUserId: 'user-1' }, expectedUpdatedAt: CONTENT_UPDATED_AT, secretProvenancePolicy: { mode: 'replace', provenance: { status: 'exact', entries: [] } }, } @@ -1897,6 +1898,7 @@ describe('file manage operations', () => { Buffer.from('secret-value'), 'text/plain', { + version: { source: 'workflow', authorUserId: 'user-1' }, expectedUpdatedAt: CONTENT_UPDATED_AT, secretProvenancePolicy: { mode: 'replace', provenance: { status: 'unknown' } }, } @@ -1954,6 +1956,7 @@ describe('file manage operations', () => { Buffer.from('beforesecret-value'), undefined, { + version: { source: 'workflow', authorUserId: 'user-1' }, expectedUpdatedAt: CONTENT_UPDATED_AT, secretProvenancePolicy: { mode: 'replace', @@ -2004,6 +2007,7 @@ describe('file manage operations', () => { Buffer.from('beforeordinary text'), undefined, { + version: { source: 'workflow', authorUserId: 'user-1' }, expectedUpdatedAt: CONTENT_UPDATED_AT, secretProvenancePolicy: { mode: 'preserve' }, } diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts new file mode 100644 index 00000000000..27a113b6495 --- /dev/null +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -0,0 +1,431 @@ +/** Real PostgreSQL transactions and local object storage for workspace file version history. */ +import { mkdtempSync } from 'node:fs' +import { access, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { db } from '@sim/db' +import { + organization, + user, + workspace, + workspaceFileSecretProvenance, + workspaceFileVersion, +} from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { asc, eq, inArray, sql } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +const fixtureStorage = vi.hoisted(() => ({ root: '' })) +vi.mock('@/lib/uploads/core/setup.server', () => ({ + get UPLOAD_DIR_SERVER() { + return fixtureStorage.root + }, +})) + +import { + createKnowledgeAclFixtureIds, + seedKnowledgeAclFixture, +} from '@/lib/knowledge/__integration__/seed-source-access-fixture' +import { + deleteWorkspaceFileVersion, + fetchWorkspaceFileBuffer, + getWorkspaceFile, + updateWorkspaceFileContent, + uploadWorkspaceFile, +} from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { + getCurrentWorkspaceFileVersion, + queryWorkspaceFileVersions, +} from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { revertWorkspaceFileVersion } from '@/lib/workspace-files/application/file-versions' +import { runCleanupFileVersions } from '@/background/cleanup-file-versions' + +describe('workspace file version history in PostgreSQL', () => { + const fixtures: ReturnType[] = [] + + beforeAll(() => { + fixtureStorage.root = mkdtempSync(path.join(tmpdir(), 'sim-file-versions-')) + }) + + afterAll(async () => { + for (const ids of fixtures) { + await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await db.delete(organization).where(eq(organization.id, ids.organizationId)) + await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) + } + await rm(fixtureStorage.root, { recursive: true, force: true }) + await db.$client.end() + }) + + async function seedFile(content: string) { + const ids = createKnowledgeAclFixtureIds() + fixtures.push(ids) + await seedKnowledgeAclFixture(ids) + const uploaded = await uploadWorkspaceFile( + ids.workspaceId, + ids.aliceId, + Buffer.from(content), + `notes-${generateId()}.txt`, + 'text/plain', + { notifyWorkspaceChange: false } + ) + return { ...ids, fileId: uploaded.id, firstKey: uploaded.key } + } + + function versionRows(fileId: string) { + return db + .select() + .from(workspaceFileVersion) + .where(eq(workspaceFileVersion.fileId, fileId)) + .orderBy(asc(workspaceFileVersion.version)) + } + + async function objectExists(key: string) { + try { + await access(path.join(fixtureStorage.root, key)) + return true + } catch { + return false + } + } + + async function readVersionBytes(workspaceId: string, fileId: string, key: string) { + const file = await getWorkspaceFile(workspaceId, fileId) + if (!file) throw new Error('file missing') + return (await fetchWorkspaceFileBuffer({ ...file, key }, { maxBytes: 1024 })).toString() + } + + it('lists a never-rewritten file as an implicit version 1 attributed to its uploader', async () => { + const fixture = await seedFile('original') + const file = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + if (!file) throw new Error('file missing') + + const { versions } = await queryWorkspaceFileVersions(file, { sortOrder: 'desc', limit: 10 }) + + expect(versions).toHaveLength(1) + expect(versions[0]).toMatchObject({ + version: 1, + isCurrent: true, + key: fixture.firstKey, + source: 'upload', + authorUserIds: [fixture.aliceId], + }) + expect(await versionRows(fixture.fileId)).toEqual([]) + }) + + it('materializes version 1 on the first write and keeps the outgoing bytes readable', async () => { + const fixture = await seedFile('original') + + const updated = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('second'), + undefined, + { version: { source: 'api', authorUserId: fixture.bobId } } + ) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => [row.version, row.source, row.authorUserIds])).toEqual([ + [1, 'upload', [fixture.aliceId]], + [2, 'api', [fixture.bobId]], + ]) + expect(rows[0].key).toBe(fixture.firstKey) + expect(rows[0].supersededAt).not.toBeNull() + expect(rows[1].key).toBe(updated.key) + expect(rows[1].supersededAt).toBeNull() + expect(await objectExists(fixture.firstKey)).toBe(true) + expect(await readVersionBytes(fixture.workspaceId, fixture.fileId, rows[0].key)).toBe( + 'original' + ) + }) + + it('folds consecutive collaborative writes into one version and deletes the replaced object', async () => { + const fixture = await seedFile('original') + const write = { source: 'collab', authorUserId: fixture.aliceId } as const + + const first = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('draft one'), + undefined, + { version: write } + ) + const second = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.bobId, + Buffer.from('draft two'), + undefined, + { version: { source: 'collab', authorUserId: fixture.bobId } } + ) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => row.version)).toEqual([1, 2]) + expect(rows[1]).toMatchObject({ + key: second.key, + source: 'collab', + authorUserIds: [fixture.aliceId, fixture.bobId], + supersededAt: null, + }) + expect(await objectExists(first.key)).toBe(false) + expect(await objectExists(fixture.firstKey)).toBe(true) + }) + + it('opens a new collaborative version once the window or the idle gap has passed', async () => { + const fixture = await seedFile('original') + const write = { source: 'collab', authorUserId: fixture.aliceId } as const + const persist = (content: string) => + updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from(content), + undefined, + { version: write } + ) + + await persist('session one') + await db + .update(workspaceFileVersion) + .set({ createdAt: sql`now() - interval '11 minutes'` }) + .where( + sql`${workspaceFileVersion.fileId} = ${fixture.fileId} AND ${workspaceFileVersion.version} = 2` + ) + await persist('session one, later') + await db + .update(workspaceFileVersion) + .set({ updatedAt: sql`now() - interval '6 minutes'` }) + .where( + sql`${workspaceFileVersion.fileId} = ${fixture.fileId} AND ${workspaceFileVersion.version} = 3` + ) + await persist('session two') + + expect((await versionRows(fixture.fileId)).map((row) => row.version)).toEqual([1, 2, 3, 4]) + }) + + it('reports the current version number before and after the first write', async () => { + const fixture = await seedFile('original') + const before = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + if (!before) throw new Error('file missing') + expect((await getCurrentWorkspaceFileVersion(before)).version).toBe(1) + + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('second'), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + + const after = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + if (!after) throw new Error('file missing') + expect((await getCurrentWorkspaceFileVersion(after)).version).toBe(2) + }) + + it('does not keep an empty shell as a version of its own', async () => { + const fixture = await seedFile('') + + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('first real content'), + undefined, + { version: { source: 'copilot', authorUserId: fixture.aliceId } } + ) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => [row.version, row.source])).toEqual([[1, 'copilot']]) + expect(await objectExists(fixture.firstKey)).toBe(false) + }) + + it('deletes a superseded version and its object but never the current one', async () => { + const fixture = await seedFile('original') + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('second'), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + + await expect(deleteWorkspaceFileVersion(fixture.workspaceId, fixture.fileId, 2)).resolves.toBe( + false + ) + await expect(deleteWorkspaceFileVersion(fixture.workspaceId, fixture.fileId, 1)).resolves.toBe( + true + ) + + expect((await versionRows(fixture.fileId)).map((row) => row.version)).toEqual([2]) + expect(await objectExists(fixture.firstKey)).toBe(false) + }) + + it('never folds deliberate writes, and repoints the head for identical bytes', async () => { + const fixture = await seedFile('original') + const write = { source: 'api', authorUserId: fixture.aliceId } as const + const first = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('changed'), + undefined, + { version: write } + ) + const identical = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('changed'), + undefined, + { version: write } + ) + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('changed again'), + undefined, + { version: write } + ) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => row.version)).toEqual([1, 2, 3]) + expect(rows[1].key).toBe(identical.key) + expect(await objectExists(first.key)).toBe(false) + }) + + it('numbers concurrent writes without gaps or collisions', async () => { + const fixture = await seedFile('original') + await Promise.all( + ['a', 'b', 'c', 'd'].map((content) => + updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from(content), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + ) + ) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5]) + expect(rows.filter((row) => row.supersededAt === null)).toHaveLength(1) + const current = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + expect(rows.at(-1)?.key).toBe(current?.key) + }) + + it('reverts to an earlier version as a new version carrying that version’s provenance', async () => { + const fixture = await seedFile('original') + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('tainted'), + undefined, + { + version: { source: 'api', authorUserId: fixture.aliceId }, + secretProvenancePolicy: { mode: 'replace', provenance: { status: 'unknown' } }, + } + ) + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('clean'), + undefined, + { + version: { source: 'api', authorUserId: fixture.aliceId }, + secretProvenancePolicy: { mode: 'replace', provenance: { status: 'exact', entries: [] } }, + } + ) + + const result = await revertWorkspaceFileVersion.execute({ + principal: { kind: 'session', userId: fixture.aliceId, sessionId: generateId() }, + input: { fileId: fixture.fileId, assertedWorkspaceId: fixture.workspaceId, version: 2 }, + }) + + expect(result).toMatchObject({ reverted: true, revertedFrom: 3, revertedTo: 2 }) + expect(result.version).toMatchObject({ + version: 4, + source: 'revert', + restoredFromVersion: 2, + authorUserIds: [fixture.aliceId], + }) + expect(await readVersionBytes(fixture.workspaceId, fixture.fileId, result.file.key)).toBe( + 'tainted' + ) + const [sidecar] = await db + .select({ status: workspaceFileSecretProvenance.status }) + .from(workspaceFileSecretProvenance) + .where(eq(workspaceFileSecretProvenance.fileId, fixture.fileId)) + expect(sidecar.status).toBe('unknown') + + const noop = await revertWorkspaceFileVersion.execute({ + principal: { kind: 'session', userId: fixture.aliceId, sessionId: generateId() }, + input: { fileId: fixture.fileId, assertedWorkspaceId: fixture.workspaceId, version: 4 }, + }) + expect(noop).toMatchObject({ reverted: false }) + expect(await versionRows(fixture.fileId)).toHaveLength(4) + }) + + it('refuses a revert whose expected current version is stale', async () => { + const fixture = await seedFile('original') + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('second'), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + + await expect( + revertWorkspaceFileVersion.execute({ + principal: { kind: 'session', userId: fixture.aliceId, sessionId: generateId() }, + input: { + fileId: fixture.fileId, + assertedWorkspaceId: fixture.workspaceId, + version: 1, + expectedCurrentVersion: 1, + }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + }) + + it('prunes superseded versions past retention but always keeps the newest ten', async () => { + const fixture = await seedFile('v1') + for (let index = 2; index <= 13; index++) { + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from(`v${index}`), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + } + await db + .update(workspaceFileVersion) + .set({ supersededAt: sql`now() - interval '400 days'` }) + .where( + sql`${workspaceFileVersion.fileId} = ${fixture.fileId} AND ${workspaceFileVersion.supersededAt} IS NOT NULL` + ) + + await runCleanupFileVersions({ + plan: 'pro', + workspaceIds: [fixture.workspaceId], + retentionHours: 180 * 24, + label: 'file-versions-integration', + }) + + const rows = await versionRows(fixture.fileId) + expect(rows.map((row) => row.version)).toEqual([4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + expect(rows.at(-1)?.supersededAt).toBeNull() + }) +}) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index b76fbdc7a17..f191b572c93 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -5,7 +5,13 @@ import { randomBytes } from 'crypto' import { db } from '@sim/db' -import { uploadSession, type WorkspaceFileRow, workspace, workspaceFiles } from '@sim/db/schema' +import { + uploadSession, + type WorkspaceFileRow, + workspace, + workspaceFiles, + workspaceFileVersion, +} from '@sim/db/schema' import { createLogger } from '@sim/logger' import { describeError, @@ -65,14 +71,25 @@ import { EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE, initializeWorkspaceFileSecretProvenanceInTx, preserveWorkspaceFileSecretProvenanceInTx, + reinstateWorkspaceFileSecretProvenanceInTx, replaceWorkspaceFileSecretProvenanceInTx, + snapshotWorkspaceFileSecretProvenanceInTx, type WorkspaceFileSecretProvenance, type WorkspaceFileSecretProvenancePolicy, } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' import { enqueueWorkspaceFileStorageCleanup, - processWorkspaceFileStorageCleanupNow, + enqueueWorkspaceFileStorageCleanups, + processWorkspaceFileStorageCleanupsNow, } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' +import { + deleteWorkspaceFileVersionInTx, + hashWorkspaceFileContent, + isVersionHeadCurrent, + loadWorkspaceFileVersionHead, + recordWorkspaceFileVersionInTx, + type WorkspaceFileVersionWrite, +} from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { buildStorageKeySegment } from '@/lib/uploads/core/storage-key' import { deleteFile, @@ -1732,7 +1749,9 @@ export async function fetchWorkspaceFileBuffer( // transport failure to answer with their own placeholder, and re-wrapping it in a // plain Error would erase the only thing that tells the two apart. if (isPayloadSizeLimitError(error)) throw error - throw new Error(`Failed to download file: ${getErrorMessage(error, 'Unknown error')}`) + throw new Error(`Failed to download file: ${getErrorMessage(error, 'Unknown error')}`, { + cause: error, + }) } } @@ -1757,8 +1776,10 @@ export async function updateWorkspaceFileContent( fileId: string, userId: string, content: Buffer, - contentType?: string, - options?: { + contentType: string | undefined, + options: { + /** How this write is recorded in the file's version history. */ + version: WorkspaceFileVersionWrite /** * Whether to stream this write into any open collaborative editor as a live CRDT merge. Defaults * to `true`, so EVERY external write path (copilot tools, the file tool, the content route) reaches @@ -1784,8 +1805,8 @@ export async function updateWorkspaceFileContent( */ secretProvenancePolicy?: WorkspaceFileSecretProvenancePolicy } -): Promise { - if (options?.collabDocState && !options.expectedUpdatedAt) { +): Promise { + if (options.collabDocState && !options.expectedUpdatedAt) { throw new Error('Collaborative state updates require an expected content version') } logger.info(`Updating workspace file content: ${fileId} for workspace ${workspaceId}`) @@ -1798,6 +1819,7 @@ export async function updateWorkspaceFileContent( const storageBillingContext = await resolveStorageBillingContext(workspaceId) const nextContentType = contentType || fileRecord.type const nextStorageKey = generateWorkspaceFileKey(workspaceId, fileRecord.name) + const contentHash = hashWorkspaceFileContent(content) try { const metadata: Record = { @@ -1822,10 +1844,11 @@ export async function updateWorkspaceFileContent( let finalized: { file: WorkspaceFileRow - oldKey: string sizeDiff: number updatedUsage: number | undefined liveDocEventId: string | undefined + storageCleanupEventIds: string[] + currentVersion: number } try { finalized = await db.transaction(async (tx) => { @@ -1854,16 +1877,26 @@ export async function updateWorkspaceFileContent( // reconcile stale durable content and clobber in-flight edits. Coalesce to `updatedAt` for rows // predating the column. A mismatch means the CONTENT changed out-of-band; abort rather than clobber. if ( - options?.expectedUpdatedAt && + options.expectedUpdatedAt && currentFile.contentUpdatedAt.getTime() !== options.expectedUpdatedAt.getTime() ) { throw new ContentVersionConflictError(fileId) } - if (options?.collabDocState) { + if (options.collabDocState) { await saveCollabDocStateInTx(tx, fileId, options.collabDocState) } + const versionHead = await loadWorkspaceFileVersionHead(fileId, tx) + const previousProvenance = isVersionHeadCurrent(versionHead, currentFile) + ? undefined + : await snapshotWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + currentFile.contentUpdatedAt, + currentFile.secretProvenanceVersion + ) + const sizeDiff = content.length - getWorkspaceFileSize(currentFile) const now = new Date() // `contentUpdatedAt` is the persist If-Match token, so it MUST be strictly monotonic per file — a @@ -1904,14 +1937,21 @@ export async function updateWorkspaceFileContent( throw new OrchestrationError('not_found', 'File not found or could not be updated') } - if (options?.secretProvenancePolicy?.mode === 'replace') { + if (options.secretProvenancePolicy?.mode === 'replace') { await replaceWorkspaceFileSecretProvenanceInTx( tx, fileId, updatedFile.contentUpdatedAt, options.secretProvenancePolicy.provenance ) - } else if (options?.secretProvenancePolicy?.mode === 'preserve') { + } else if (options.secretProvenancePolicy?.mode === 'reinstate') { + await reinstateWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + updatedFile.contentUpdatedAt, + options.secretProvenancePolicy.snapshot + ) + } else if (options.secretProvenancePolicy?.mode === 'preserve') { await preserveWorkspaceFileSecretProvenanceInTx( tx, fileId, @@ -1925,6 +1965,31 @@ export async function updateWorkspaceFileContent( }) } + /** Every policy but `preserve` binds a sidecar, which marks the file tracked. */ + const secretProvenanceVersion = + options.secretProvenancePolicy?.mode === 'preserve' + ? currentFile.secretProvenanceVersion + : 1 + const recorded = await recordWorkspaceFileVersionInTx(tx, { + head: versionHead, + previous: currentFile, + previousProvenance, + next: updatedFile, + nextProvenance: await snapshotWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + updatedFile.contentUpdatedAt, + secretProvenanceVersion + ), + contentHash, + write: options.version, + now, + }) + const storageCleanupEventIds = await enqueueWorkspaceFileStorageCleanups( + tx, + recorded.releasedKeys + ) + let updatedUsage: number | undefined if (sizeDiff > 0) { updatedUsage = await incrementStorageUsageForBillingContextInTx( @@ -1941,7 +2006,7 @@ export async function updateWorkspaceFileContent( } const liveDocEventId = - options?.syncLiveDoc !== false && + options.syncLiveDoc !== false && (isMarkdownFile({ type: currentFile.contentType, name: currentFile.originalName }) || isMarkdownFile({ type: updatedFile.contentType, name: updatedFile.originalName })) ? await enqueueWorkspaceFileLiveDocReconciliation(tx, { @@ -1953,10 +2018,11 @@ export async function updateWorkspaceFileContent( return { file: updatedFile, - oldKey: currentFile.key, sizeDiff, updatedUsage, liveDocEventId, + storageCleanupEventIds, + currentVersion: recorded.version, } }) } catch (finalizationError) { @@ -1971,9 +2037,11 @@ export async function updateWorkspaceFileContent( finalized.sizeDiff < 0 ) } - if (finalized.oldKey !== uploadResult.key) { - await cleanupWorkspaceStorageObject(finalized.oldKey, 'version replacement') - } + await processWorkspaceFileStorageCleanupsNow(finalized.storageCleanupEventIds, { + workspaceId, + fileId, + reason: 'released version', + }) if (finalized.liveDocEventId) { try { @@ -2017,6 +2085,7 @@ export async function updateWorkspaceFileContent( uploadedAt: finalized.file.uploadedAt, updatedAt: finalized.file.updatedAt, contentUpdatedAt: finalized.file.contentUpdatedAt, + currentVersion: finalized.currentVersion, } } catch (error) { // Preserve the typed conflict so callers can catch it and reconcile — it's an expected outcome of @@ -2040,6 +2109,45 @@ export async function updateWorkspaceFileContent( } } +/** + * Deletes one superseded version of an active workspace file and releases its stored object. The + * file row is locked so the delete serializes with content writes that renumber or prune history. + * Returns false when the file has no such superseded version (it never existed, retention removed + * it, or it is the current version). + */ +export async function deleteWorkspaceFileVersion( + workspaceId: string, + fileId: string, + version: number +): Promise { + const cleanupEventId = await db.transaction(async (tx) => { + const [file] = await tx + .select({ id: workspaceFiles.id }) + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.id, fileId), + eq(workspaceFiles.workspaceId, workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .for('update') + .limit(1) + if (!file) throw new OrchestrationError('not_found', 'File not found') + const key = await deleteWorkspaceFileVersionInTx(tx, fileId, version) + return key ? enqueueWorkspaceFileStorageCleanup(tx, { key }) : null + }) + if (!cleanupEventId) return false + + await processWorkspaceFileStorageCleanupsNow([cleanupEventId], { + workspaceId, + fileId, + reason: 'deleted version', + }) + return true +} + /** * Rename a workspace file (updates the display name in the database) */ @@ -2237,7 +2345,7 @@ export async function purgeCreatedWorkspaceFile(params: { eq(workspaceFiles.context, 'workspace'), isNull(workspaceFiles.deletedAt) ) - const cleanupEventId = await db.transaction(async (tx) => { + const cleanupEventIds = await db.transaction(async (tx) => { const [lockedFile] = await tx .select({ id: workspaceFiles.id, @@ -2250,6 +2358,11 @@ export async function purgeCreatedWorkspaceFile(params: { .limit(1) if (!lockedFile) return null + const versionKeys = await tx + .select({ key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where(eq(workspaceFileVersion.fileId, lockedFile.id)) + const [deleted] = await tx .delete(workspaceFiles) .where(matchesCreatedFile) @@ -2261,28 +2374,16 @@ export async function purgeCreatedWorkspaceFile(params: { storageBillingContext, getWorkspaceFileSize(lockedFile) ) - return enqueueWorkspaceFileStorageCleanup(tx, { key: lockedFile.key }) + const keys = new Set([lockedFile.key, ...versionKeys.map((row) => row.key)]) + return enqueueWorkspaceFileStorageCleanups(tx, [...keys]) }) - if (!cleanupEventId) return false + if (!cleanupEventIds) return false - try { - const result = await processWorkspaceFileStorageCleanupNow(cleanupEventId) - if (result !== 'completed') { - logger.warn('Archive rollback storage cleanup deferred to outbox retry', { - workspaceId: params.workspaceId, - fileId: params.fileId, - cleanupEventId, - result, - }) - } - } catch (error) { - logger.warn('Archive rollback storage cleanup deferred after inline processing error', { - workspaceId: params.workspaceId, - fileId: params.fileId, - cleanupEventId, - error: getErrorMessage(error), - }) - } + await processWorkspaceFileStorageCleanupsNow(cleanupEventIds, { + workspaceId: params.workspaceId, + fileId: params.fileId, + reason: 'archive rollback', + }) return true } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts index f3459324a8a..1ca02c7a183 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.test.ts @@ -5,9 +5,14 @@ import { workspaceFileSecretProvenance, workspaceFiles } from '@sim/db/schema' import { dbChainMock, dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockReportWrite, mockReportRefusal } = vi.hoisted(() => ({ +const { mockReportWrite, mockReportRefusal, mockFindWorkspaceFileVersionKeys } = vi.hoisted(() => ({ mockReportWrite: vi.fn(), mockReportRefusal: vi.fn(), + mockFindWorkspaceFileVersionKeys: vi.fn(async () => new Set()), +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + findWorkspaceFileVersionKeys: mockFindWorkspaceFileVersionKeys, })) vi.mock('@/lib/execution/durable-secret-provenance-telemetry', () => ({ @@ -980,6 +985,31 @@ describe('workspace file secret provenance', () => { expect(dbChainMockFns.where).toHaveBeenCalledTimes(1) }) + it('refuses a retained version key rather than treating it as an untracked legacy key', async () => { + queueTableRows(workspaceFiles, []) + mockFindWorkspaceFileVersionKeys.mockResolvedValueOnce(new Set(['old-version-key'])) + + await expect( + areModelSafeWorkspaceFileKeys(['old-version-key'], { workspaceId: 'workspace-1' }) + ).resolves.toBe(false) + expect(mockFindWorkspaceFileVersionKeys).toHaveBeenCalledWith(['old-version-key']) + }) + + it('drops a retained version key from model attachments', async () => { + queueTableRows(workspaceFiles, []) + mockFindWorkspaceFileVersionKeys.mockResolvedValueOnce(new Set(['old-version-key'])) + + await expect( + filterModelSafeWorkspaceFileAttachments( + [ + { id: 'file-1', key: 'old-version-key' }, + { id: 'legacy', key: 'legacy-key' }, + ], + { workspaceId: 'workspace-1' } + ) + ).resolves.toEqual([{ id: 'legacy', key: 'legacy-key' }]) + }) + it('rejects a document batch when any canonical workspace file is tainted', async () => { queueTableRows(workspaceFiles, [ { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts index 2ca4e4299f4..ef23bd9aef5 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts @@ -20,6 +20,7 @@ import { PROVENANCE_MAX_ENTRIES, PROVENANCE_MAX_SERIALIZED_BYTES, } from '@/lib/execution/provenance-limits' +import { findWorkspaceFileVersionKeys } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { isResolvedSecretProvenanceAbsence, type ResolvedSecretTraceProvenanceV1, @@ -54,6 +55,8 @@ export const EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE = Object.freeze({ export type WorkspaceFileSecretProvenancePolicy = | { mode: 'replace'; provenance: WorkspaceFileSecretProvenance } | { mode: 'preserve' } + /** Restores the classification captured with a previous version's bytes (a revert). */ + | { mode: 'reinstate'; snapshot: WorkspaceFileSecretProvenanceSnapshot } export type WorkspaceFileSecretProvenanceWriteDecision = | { safe: true; provenance: WorkspaceFileSecretProvenance } @@ -571,6 +574,71 @@ export async function preserveWorkspaceFileSecretProvenanceInTx( await markWorkspaceFileSecretProvenanceTrackedInTx(tx, fileId, nextContentUpdatedAt) } +/** + * Stored provenance for one content version, detached from the per-file sidecar. `status: null` + * marks bytes that predate tracking and read as exact-empty, like a NULL tracking marker. + */ +export interface WorkspaceFileSecretProvenanceSnapshot { + status: 'exact' | 'unknown' | 'unrecorded' | null + entries: StoredWorkspaceFileSecretProvenanceEntry[] +} + +/** + * Captures the sidecar bound to `contentUpdatedAt` without interpreting it. A sidecar bound to any + * other version, a malformed one, or a missing one under a tracked marker snapshots as unknown, so a + * later reinstatement can never be more permissive than a reader would have been for those bytes. + */ +export async function snapshotWorkspaceFileSecretProvenanceInTx( + tx: DbTransaction, + fileId: string, + contentUpdatedAt: Date, + secretProvenanceVersion: number | null +): Promise { + if (secretProvenanceVersion === null) return { status: null, entries: [] } + if (secretProvenanceVersion !== 1) return { status: 'unknown', entries: [] } + const [stored] = await tx + .select({ + contentUpdatedAt: workspaceFileSecretProvenance.contentUpdatedAt, + status: workspaceFileSecretProvenance.status, + entries: workspaceFileSecretProvenance.entries, + }) + .from(workspaceFileSecretProvenance) + .where(eq(workspaceFileSecretProvenance.fileId, fileId)) + .limit(1) + if ( + !stored || + stored.contentUpdatedAt.getTime() !== contentUpdatedAt.getTime() || + !isValidStoredEntries(stored.entries) + ) { + return { status: 'unknown', entries: [] } + } + if (stored.status === 'exact') return { status: 'exact', entries: stored.entries } + if (stored.status === 'unrecorded') return { status: 'unrecorded', entries: [] } + return { status: 'unknown', entries: [] } +} + +/** + * Binds a previously captured snapshot to the file's new content version, writing the stored entries + * verbatim so a revert restores exactly the classification its bytes carried. An untracked snapshot + * binds as exact-empty — the reading an untracked marker already produced for those bytes. + */ +export async function reinstateWorkspaceFileSecretProvenanceInTx( + tx: DbTransaction, + fileId: string, + contentUpdatedAt: Date, + snapshot: WorkspaceFileSecretProvenanceSnapshot +): Promise { + const provenance: WorkspaceFileSecretProvenance = + snapshot.status === null + ? EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE + : snapshot.status !== 'exact' + ? { status: snapshot.status } + : isValidStoredEntries(snapshot.entries) + ? { status: 'exact', entries: deserializeExactEntriesFromStorage(snapshot.entries) } + : { status: 'unknown' } + await replaceWorkspaceFileSecretProvenanceInTx(tx, fileId, contentUpdatedAt, provenance) +} + /** Copies exact provenance for a byte-identical, same-owner-scope file copy; otherwise unknown. */ export async function copyWorkspaceFileSecretProvenanceInTx( tx: DbTransaction, @@ -1024,12 +1092,17 @@ export async function filterModelSafeWorkspaceFileAttachments< const rows = await loadModelSafeWorkspaceFileRows(keys) const rowByKey = new Map(rows.map((row) => [row.key, row])) + const versionKeys = await findWorkspaceFileVersionKeys(keys.filter((key) => !rowByKey.has(key))) let unrecorded = 0 let refused = 0 const kept = attachments.filter((attachment) => { if (typeof attachment.key !== 'string' || attachment.key.length === 0) return true const row = rowByKey.get(attachment.key) - if (!row) return true + if (!row) { + if (!versionKeys.has(attachment.key)) return true + refused += 1 + return false + } if ( row.context !== 'workspace' && row.context !== 'mothership' && @@ -1136,6 +1209,16 @@ export async function areModelSafeWorkspaceFileKeys( } const rows = await loadModelSafeWorkspaceFileRows(uniqueKeys) + const rowKeys = new Set(rows.map((row) => row.key)) + const versionKeys = await findWorkspaceFileVersionKeys( + uniqueKeys.filter((key) => !rowKeys.has(key)) + ) + if (versionKeys.size > 0) { + return refuseWorkspaceFileProvenance( + 'workspace-file-provenance-unavailable', + options.workspaceId + ) + } let unrecorded = 0 for (const row of rows) { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts index 5a080ec06df..38198588f4d 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts @@ -31,6 +31,7 @@ const { mockReplaceWorkspaceFileSecretProvenanceInTx, mockSaveCollabDocStateInTx, mockUploadFile, + mockRecordWorkspaceFileVersionInTx, } = vi.hoisted(() => ({ mockDecrementStorageUsageForBillingContextInTx: vi.fn(), mockDeleteFile: vi.fn(), @@ -54,6 +55,7 @@ const { mockReplaceWorkspaceFileSecretProvenanceInTx: vi.fn(), mockSaveCollabDocStateInTx: vi.fn(), mockUploadFile: vi.fn(), + mockRecordWorkspaceFileVersionInTx: vi.fn(), })) vi.mock('@/lib/collab-doc/collab-state', () => ({ @@ -65,7 +67,16 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', () EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE: { status: 'exact', entries: [] }, initializeWorkspaceFileSecretProvenanceInTx: mockInitializeWorkspaceFileSecretProvenanceInTx, preserveWorkspaceFileSecretProvenanceInTx: vi.fn(), + reinstateWorkspaceFileSecretProvenanceInTx: vi.fn(), replaceWorkspaceFileSecretProvenanceInTx: mockReplaceWorkspaceFileSecretProvenanceInTx, + snapshotWorkspaceFileSecretProvenanceInTx: vi.fn(async () => ({ status: 'exact', entries: [] })), +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + hashWorkspaceFileContent: vi.fn(() => 'content-hash'), + isVersionHeadCurrent: vi.fn(() => false), + loadWorkspaceFileVersionHead: vi.fn(async () => undefined), + recordWorkspaceFileVersionInTx: mockRecordWorkspaceFileVersionInTx, })) vi.mock('@/lib/realtime/notify', () => ({ @@ -98,7 +109,14 @@ vi.mock('@/lib/uploads/core/storage-service', () => ({ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox', () => ({ enqueueWorkspaceFileStorageCleanup: mockEnqueueWorkspaceFileStorageCleanup, + enqueueWorkspaceFileStorageCleanups: (executor: unknown, keys: string[]) => + Promise.all(keys.map((key) => mockEnqueueWorkspaceFileStorageCleanup(executor, { key }))), processWorkspaceFileStorageCleanupNow: mockProcessWorkspaceFileStorageCleanupNow, + processWorkspaceFileStorageCleanupsNow: async (eventIds: string[]) => { + for (const eventId of eventIds) { + await mockProcessWorkspaceFileStorageCleanupNow(eventId).catch(() => undefined) + } + }, })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-folder-manager', () => ({ @@ -139,6 +157,8 @@ import { uploadWorkspaceFile, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +const TEST_VERSION_WRITE = { source: 'api', authorUserId: 'user-1' } as const + const STORAGE_CONTEXT = { workspaceId: '7727ef3f-8cf6-4686-b063-2bb006a10785', billedAccountUserId: 'workspace-owner', @@ -190,6 +210,7 @@ describe('workspace file metadata and storage accounting', () => { mockProcessWorkspaceFileLiveDocReconciliationNow.mockResolvedValue('completed') mockReplaceWorkspaceFileSecretProvenanceInTx.mockResolvedValue(undefined) mockSaveCollabDocStateInTx.mockResolvedValue(undefined) + mockRecordWorkspaceFileVersionInTx.mockResolvedValue({ releasedKeys: [] }) }) it('returns the canonical inserted record with the pre-resolved folder path', async () => { @@ -705,7 +726,8 @@ describe('workspace file metadata and storage accounting', () => { FILE_ROW.id, FILE_ROW.userId, Buffer.alloc(10), - 'application/octet-stream' + 'application/octet-stream', + { version: TEST_VERSION_WRITE } ) expect(mockUploadFile).toHaveBeenCalledWith( @@ -725,14 +747,57 @@ describe('workspace file metadata and storage accounting', () => { updatedFile.contentUpdatedAt, { status: 'unknown' } ) - expect(mockDeleteFile).toHaveBeenCalledWith({ key: FILE_ROW.key, context: 'workspace' }) + expect(mockRecordWorkspaceFileVersionInTx).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + previous: concurrentFile, + next: updatedFile, + contentHash: 'content-hash', + write: TEST_VERSION_WRITE, + }) + ) + expect(mockDeleteFile).not.toHaveBeenCalled() + expect(mockEnqueueWorkspaceFileStorageCleanup).not.toHaveBeenCalled() expect(updated.key).toBe(replacementKey) expect(mockUploadFile.mock.invocationCallOrder[0]).toBeLessThan( dbChainMockFns.transaction.mock.invocationCallOrder[0] ) - expect(dbChainMockFns.transaction.mock.invocationCallOrder[0]).toBeLessThan( - mockDeleteFile.mock.invocationCallOrder[0] + }) + + it('durably releases the storage keys the version history no longer references', async () => { + const transaction = { ...dbChainMock.db } + const replacementKey = `${FILE_ROW.key}-replacement` + const updatedFile = { ...FILE_ROW, key: replacementKey, size: 10, sizeBytes: 10 } + let committed = false + dbChainMockFns.transaction.mockImplementationOnce(async (callback) => { + const result = await callback(transaction) + expect(mockEnqueueWorkspaceFileStorageCleanup).toHaveBeenCalledWith(transaction, { + key: FILE_ROW.key, + }) + expect(mockProcessWorkspaceFileStorageCleanupNow).not.toHaveBeenCalled() + committed = true + return result + }) + mockProcessWorkspaceFileStorageCleanupNow.mockImplementationOnce(async () => { + expect(committed).toBe(true) + return 'completed' + }) + dbChainMockFns.limit.mockResolvedValueOnce([FILE_ROW]).mockResolvedValueOnce([FILE_ROW]) + dbChainMockFns.returning.mockResolvedValueOnce([updatedFile]) + mockUploadFile.mockResolvedValueOnce({ key: replacementKey }) + mockRecordWorkspaceFileVersionInTx.mockResolvedValueOnce({ releasedKeys: [FILE_ROW.key] }) + + await updateWorkspaceFileContent( + FILE_ROW.workspaceId, + FILE_ROW.id, + FILE_ROW.userId, + Buffer.alloc(10), + undefined, + { version: { source: 'collab', authorUserId: FILE_ROW.userId } } ) + + expect(mockProcessWorkspaceFileStorageCleanupNow).toHaveBeenCalledWith('cleanup-event-1') + expect(mockDeleteFile).not.toHaveBeenCalled() }) it('cleans up only the new overwrite object when atomic finalization fails', async () => { @@ -750,7 +815,9 @@ describe('workspace file metadata and storage accounting', () => { FILE_ROW.workspaceId, FILE_ROW.id, FILE_ROW.userId, - Buffer.alloc(10) + Buffer.alloc(10), + undefined, + { version: TEST_VERSION_WRITE } ) ).rejects.toThrow('Storage limit exceeded') @@ -792,15 +859,13 @@ describe('workspace file metadata and storage accounting', () => { committed = true return result }) - mockDeleteFile.mockImplementationOnce(async () => { - expect(committed).toBe(true) - }) dbChainMockFns.limit.mockResolvedValueOnce([MD_ROW]).mockResolvedValueOnce([MD_ROW]) dbChainMockFns.returning.mockResolvedValueOnce([updatedFile]) mockUploadFile.mockResolvedValueOnce({ key: replacementKey }) await expect( updateWorkspaceFileContent(MD_ROW.workspaceId, MD_ROW.id, MD_ROW.userId, content, undefined, { + version: TEST_VERSION_WRITE, expectedUpdatedAt: MD_ROW.contentUpdatedAt, collabDocState: PREPARED_COLLAB_STATE, }) @@ -817,7 +882,8 @@ describe('workspace file metadata and storage accounting', () => { expect(mockSaveCollabDocStateInTx.mock.invocationCallOrder[0]).toBeLessThan( dbChainMockFns.update.mock.invocationCallOrder[0] ) - expect(mockDeleteFile).toHaveBeenCalledWith({ key: MD_ROW.key, context: 'workspace' }) + expect(committed).toBe(true) + expect(mockDeleteFile).not.toHaveBeenCalled() }) it.each([ @@ -856,7 +922,11 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.userId, Buffer.from('# new content'), undefined, - { expectedUpdatedAt: MD_ROW.contentUpdatedAt, collabDocState: preparedState } + { + version: TEST_VERSION_WRITE, + expectedUpdatedAt: MD_ROW.contentUpdatedAt, + collabDocState: preparedState, + } ) ).rejects.toBe(conflict) @@ -883,7 +953,7 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.userId, Buffer.from('# new content'), undefined, - { collabDocState: PREPARED_COLLAB_STATE } + { version: TEST_VERSION_WRITE, collabDocState: PREPARED_COLLAB_STATE } ) ).rejects.toThrow('Collaborative state updates require an expected content version') @@ -905,6 +975,7 @@ describe('workspace file metadata and storage accounting', () => { Buffer.from('# stale content'), undefined, { + version: TEST_VERSION_WRITE, expectedUpdatedAt: new Date('2020-01-01T00:00:00.000Z'), collabDocState: PREPARED_COLLAB_STATE, } @@ -944,7 +1015,11 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.userId, Buffer.from('# new content'), undefined, - { expectedUpdatedAt: MD_ROW.contentUpdatedAt, collabDocState: PREPARED_COLLAB_STATE } + { + version: TEST_VERSION_WRITE, + expectedUpdatedAt: MD_ROW.contentUpdatedAt, + collabDocState: PREPARED_COLLAB_STATE, + } ) ).rejects.toThrow('accounting unavailable') @@ -1001,7 +1076,9 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.workspaceId, MD_ROW.id, MD_ROW.userId, - Buffer.from('# new content', 'utf-8') + Buffer.from('# new content', 'utf-8'), + undefined, + { version: TEST_VERSION_WRITE } ) expect(mockEnqueueWorkspaceFileLiveDocReconciliation).toHaveBeenCalledWith(transaction, { @@ -1027,7 +1104,7 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.userId, Buffer.from('# new content', 'utf-8'), undefined, - { syncLiveDoc: false } + { version: TEST_VERSION_WRITE, syncLiveDoc: false } ) expect(mockEnqueueWorkspaceFileLiveDocReconciliation).not.toHaveBeenCalled() @@ -1045,7 +1122,8 @@ describe('workspace file metadata and storage accounting', () => { FILE_ROW.id, FILE_ROW.userId, Buffer.alloc(10), - 'application/octet-stream' + 'application/octet-stream', + { version: TEST_VERSION_WRITE } ) expect(mockEnqueueWorkspaceFileLiveDocReconciliation).not.toHaveBeenCalled() @@ -1062,7 +1140,9 @@ describe('workspace file metadata and storage accounting', () => { MD_ROW.workspaceId, MD_ROW.id, MD_ROW.userId, - Buffer.alloc(size) + Buffer.alloc(size), + undefined, + { version: TEST_VERSION_WRITE } ) expect(mockEnqueueWorkspaceFileLiveDocReconciliation).toHaveBeenCalledWith(expect.anything(), { @@ -1091,7 +1171,8 @@ describe('workspace file metadata and storage accounting', () => { markdownByType.id, markdownByType.userId, Buffer.alloc(12), - 'application/octet-stream' + 'application/octet-stream', + { version: TEST_VERSION_WRITE } ) expect(mockEnqueueWorkspaceFileLiveDocReconciliation).toHaveBeenCalledWith(expect.anything(), { @@ -1113,7 +1194,7 @@ describe('workspace file metadata and storage accounting', () => { FILE_ROW.userId, Buffer.alloc(12), undefined, - { expectedUpdatedAt: FILE_ROW.updatedAt } + { version: TEST_VERSION_WRITE, expectedUpdatedAt: FILE_ROW.updatedAt } ) expect(updated.size).toBe(12) @@ -1132,7 +1213,7 @@ describe('workspace file metadata and storage accounting', () => { FILE_ROW.userId, Buffer.alloc(12), undefined, - { expectedUpdatedAt: new Date('2020-01-01T00:00:00.000Z') } + { version: TEST_VERSION_WRITE, expectedUpdatedAt: new Date('2020-01-01T00:00:00.000Z') } ) ).rejects.toBeInstanceOf(ContentVersionConflictError) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts index c47a8abd4d9..b6afaa441c4 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts @@ -3,8 +3,16 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDeleteFile } = vi.hoisted(() => ({ +const { mockDeleteFile, mockEnqueueOutboxEvents, mockProcessOutboxEventById } = vi.hoisted(() => ({ mockDeleteFile: vi.fn(), + mockEnqueueOutboxEvents: vi.fn(), + mockProcessOutboxEventById: vi.fn(), +})) + +vi.mock('@/lib/core/outbox/service', () => ({ + enqueueOutboxEvent: vi.fn(), + enqueueOutboxEvents: mockEnqueueOutboxEvents, + processOutboxEventById: mockProcessOutboxEventById, })) vi.mock('@/lib/uploads/core/storage-service', () => ({ @@ -13,6 +21,8 @@ vi.mock('@/lib/uploads/core/storage-service', () => ({ import type { OutboxEventContext } from '@/lib/core/outbox/service' import { + enqueueWorkspaceFileStorageCleanups, + processWorkspaceFileStorageCleanupsNow, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, workspaceFileStorageCleanupOutboxHandlers, } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' @@ -72,3 +82,34 @@ describe('workspace file storage cleanup outbox', () => { ) }) }) + +describe('batched workspace file storage cleanup', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('enqueues every released key in one insert', async () => { + const executor = { insert: vi.fn() } + mockEnqueueOutboxEvents.mockResolvedValueOnce(['event-a', 'event-b']) + + await expect( + enqueueWorkspaceFileStorageCleanups(executor as never, ['a', 'b']) + ).resolves.toEqual(['event-a', 'event-b']) + expect(mockEnqueueOutboxEvents).toHaveBeenCalledWith( + executor, + WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, + [{ key: 'a' }, { key: 'b' }] + ) + }) + + it('processes every event and leaves failures to the outbox worker without throwing', async () => { + mockProcessOutboxEventById + .mockRejectedValueOnce(new Error('storage unavailable')) + .mockResolvedValueOnce('completed') + + await expect( + processWorkspaceFileStorageCleanupsNow(['event-a', 'event-b'], { fileId: 'file-1' }) + ).resolves.toBeUndefined() + expect(mockProcessOutboxEventById).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts index 80dbcff4226..49a0f24a53b 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts @@ -1,13 +1,17 @@ import type { db } from '@sim/db' -import { describeError } from '@sim/utils/errors' +import { createLogger } from '@sim/logger' +import { describeError, getErrorMessage } from '@sim/utils/errors' import { enqueueOutboxEvent, + enqueueOutboxEvents, type OutboxHandler, type OutboxHandlerRegistry, processOutboxEventById, } from '@/lib/core/outbox/service' import { deleteFile } from '@/lib/uploads/core/storage-service' +const logger = createLogger('WorkspaceFileStorageCleanup') + export const WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT = 'workspace-file.storage.cleanup' interface WorkspaceFileStorageCleanupPayload { @@ -52,3 +56,39 @@ export function enqueueWorkspaceFileStorageCleanup( export function processWorkspaceFileStorageCleanupNow(eventId: string) { return processOutboxEventById(eventId, workspaceFileStorageCleanupOutboxHandlers) } + +/** Enqueues deletion of several storage objects in one insert, inside the releasing transaction. */ +export function enqueueWorkspaceFileStorageCleanups( + executor: Pick, + keys: readonly string[] +): Promise { + return enqueueOutboxEvents( + executor, + WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, + keys.map((key) => ({ key })) + ) +} + +/** + * Attempts newly committed cleanups immediately and never throws: a cleanup that cannot finish now + * stays in the outbox, whose worker retries it, so the caller's committed write is never failed. + */ +export async function processWorkspaceFileStorageCleanupsNow( + eventIds: readonly string[], + logContext: Record +): Promise { + for (const eventId of eventIds) { + try { + const result = await processWorkspaceFileStorageCleanupNow(eventId) + if (result !== 'completed') { + logger.warn('Storage cleanup deferred to outbox retry', { ...logContext, eventId, result }) + } + } catch (error) { + logger.warn('Storage cleanup deferred after inline processing error', { + ...logContext, + eventId, + error: getErrorMessage(error), + }) + } + } +} diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts new file mode 100644 index 00000000000..2958c74a47d --- /dev/null +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -0,0 +1,447 @@ +import { db } from '@sim/db' +import { + type WorkspaceFileRow, + type WorkspaceFileVersionRow, + type WorkspaceFileVersionSource, + workspaceFileVersion, +} from '@sim/db/schema' +import { sha256Hex } from '@sim/security/hash' +import { generateId } from '@sim/utils/id' +import { and, desc, eq, inArray, isNotNull } from 'drizzle-orm' +import { + type CursorKey, + type KeysetKey, + keysetColumns, + keysetPage, + type ListSortOrder, + listOrderBy, + numberKey, + resumeKeyset, +} from '@/lib/api/list-query' +import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import type { WorkspaceFileSecretProvenanceSnapshot } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' +import { getWorkspaceFileSize } from '@/lib/uploads/shared/types' + +/** A coalescing version stops absorbing writes this long after it was opened. */ +const FILE_VERSION_COALESCE_WINDOW_MS = 10 * 60 * 1000 +/** A coalescing version also closes once its writer has been idle this long. */ +const FILE_VERSION_COALESCE_IDLE_MS = 5 * 60 * 1000 +/** + * Hard ceiling on superseded versions kept per file, enforced inline on every new version so a + * write loop stays bounded between retention runs. Plan-specific counts are enforced by retention. + */ +export const MAX_SUPERSEDED_FILE_VERSIONS = 500 +/** Retention never prunes the newest versions of a file, whatever their age. */ +export const FILE_VERSION_RETENTION_KEEP_LATEST = 10 +/** + * Bounds the attribution list a long collaborative window can accumulate. Also keeps a full page of + * versions within the user-email batch the v2 presenter resolves in one query. + */ +const MAX_VERSION_AUTHORS = 10 +/** + * An implicit version 1 is attributed to the uploader only when the file was never overwritten, + * which holds when its content version still matches its upload time. + */ +const ORIGINAL_UPLOAD_TOLERANCE_MS = 1000 + +/** + * High-frequency writers whose consecutive writes fold into one version: the collaborative editor + * persists every few seconds, and a workflow can rewrite a file on every run. Every other source is + * a deliberate write and always gets its own version. + */ +const COALESCING_SOURCES: ReadonlySet = new Set(['collab', 'workflow']) + +/** How the write being committed should be recorded in the file's history. */ +export interface WorkspaceFileVersionWrite { + source: WorkspaceFileVersionSource + /** The acting user; null for actorless callers such as workspace API keys. */ + authorUserId: string | null + restoredFromVersion?: number +} + +/** sha256 (hex) of stored bytes — the identity used to detect rewrites of identical content. */ +export function hashWorkspaceFileContent(content: Buffer): string { + return sha256Hex(content) +} + +/** + * The newest version row of a file. A writer reads it inside its transaction under the file row's + * FOR UPDATE lock; a reader reads it on its own so it never pairs rows from after a concurrent first + * write with a file record from before it. + */ +export async function loadWorkspaceFileVersionHead( + fileId: string, + executor: DbOrTx = db +): Promise { + const [head] = await executor + .select() + .from(workspaceFileVersion) + .where(eq(workspaceFileVersion.fileId, fileId)) + .orderBy(desc(workspaceFileVersion.version)) + .limit(1) + return head +} + +/** + * Whether the head row describes the file's current bytes. When it does not — no rows yet, or a + * writer changed the key outside this module — the outgoing bytes must be materialized as their own + * version before the new one is recorded, so the caller snapshots their provenance first. + */ +export function isVersionHeadCurrent( + head: WorkspaceFileVersionRow | undefined, + file: Pick +): boolean { + return head !== undefined && head.supersededAt === null && head.key === file.key +} + +/** Whether a file's current bytes are still the ones originally uploaded. */ +export function isOriginalUploadContent( + file: Pick +): boolean { + return ( + Math.abs(file.contentUpdatedAt.getTime() - file.uploadedAt.getTime()) <= + ORIGINAL_UPLOAD_TOLERANCE_MS + ) +} + +function canCoalesce( + head: WorkspaceFileVersionRow, + write: WorkspaceFileVersionWrite, + now: Date +): boolean { + if (head.source !== write.source || !COALESCING_SOURCES.has(write.source)) return false + if (write.source !== 'collab' && head.authorUserIds[0] !== (write.authorUserId ?? undefined)) { + return false + } + return ( + now.getTime() - head.createdAt.getTime() < FILE_VERSION_COALESCE_WINDOW_MS && + now.getTime() - head.updatedAt.getTime() < FILE_VERSION_COALESCE_IDLE_MS + ) +} + +function mergeAuthors(existing: readonly string[], authorUserId: string | null): string[] { + if (!authorUserId || existing.includes(authorUserId)) return [...existing] + return [...existing, authorUserId].slice(0, MAX_VERSION_AUTHORS) +} + +/** The version columns that describe one content state of a file. */ +function contentColumns(file: WorkspaceFileRow, provenance: WorkspaceFileSecretProvenanceSnapshot) { + return { + key: file.key, + sizeBytes: getWorkspaceFileSize(file), + contentType: file.contentType, + contentUpdatedAt: file.contentUpdatedAt, + secretProvenanceStatus: provenance.status, + secretProvenanceEntries: provenance.entries, + } +} + +async function supersedeVersionInTx(tx: DbTransaction, versionId: string, now: Date) { + await tx + .update(workspaceFileVersion) + .set({ supersededAt: now, updatedAt: now }) + .where(eq(workspaceFileVersion.id, versionId)) +} + +interface RecordWorkspaceFileVersionParams { + /** Head row loaded before the file row was updated. */ + head: WorkspaceFileVersionRow | undefined + /** The file row as it was before this write (locked). */ + previous: WorkspaceFileRow + /** Provenance of `previous`'s bytes, captured before the provenance step; required when the head is not current. */ + previousProvenance: WorkspaceFileSecretProvenanceSnapshot | undefined + /** The file row after this write. */ + next: WorkspaceFileRow + nextProvenance: WorkspaceFileSecretProvenanceSnapshot + contentHash: string + write: WorkspaceFileVersionWrite + now: Date +} + +/** The outcome of recording one content write. */ +export interface RecordedWorkspaceFileVersion { + /** Version number of the file's current content after the write. */ + version: number + /** Storage keys no version references any more; the caller releases them after commit. */ + releasedKeys: string[] +} + +/** + * Records a committed content write in the file's history. Runs inside the content-write + * transaction, under the file row's lock, which serializes version numbering and coalescing + * decisions per file. + * + * An empty file with no history is a shell whose content arrives in this write (a create followed + * by its first content), so the shell is not kept as a version of its own. + */ +export async function recordWorkspaceFileVersionInTx( + tx: DbTransaction, + params: RecordWorkspaceFileVersionParams +): Promise { + const { previous, next, now, write } = params + let head = params.head + + if (!head && getWorkspaceFileSize(previous) === 0) { + await insertVersion(tx, params, 1) + return { version: 1, releasedKeys: [previous.key] } + } + + if (!head || !isVersionHeadCurrent(head, previous)) { + if (!params.previousProvenance) { + throw new Error('Outgoing workspace file content needs a provenance snapshot to be versioned') + } + if (head && head.supersededAt === null) await supersedeVersionInTx(tx, head.id, now) + const original = !head && isOriginalUploadContent(previous) + const [materialized] = await tx + .insert(workspaceFileVersion) + .values({ + id: generateId(), + fileId: previous.id, + workspaceId: next.workspaceId as string, + version: (head?.version ?? 0) + 1, + ...contentColumns(previous, params.previousProvenance), + contentHash: null, + source: original ? 'upload' : 'unknown', + authorUserIds: original ? [previous.userId] : [], + createdAt: previous.contentUpdatedAt, + updatedAt: previous.contentUpdatedAt, + }) + .returning() + head = materialized + } + + const nextColumns = { ...contentColumns(next, params.nextProvenance), updatedAt: now } + + if (head.contentHash === params.contentHash) { + await tx + .update(workspaceFileVersion) + .set(nextColumns) + .where(eq(workspaceFileVersion.id, head.id)) + return { version: head.version, releasedKeys: [previous.key] } + } + + if (canCoalesce(head, write, now)) { + await tx + .update(workspaceFileVersion) + .set({ + ...nextColumns, + contentHash: params.contentHash, + authorUserIds: mergeAuthors(head.authorUserIds, write.authorUserId), + }) + .where(eq(workspaceFileVersion.id, head.id)) + return { version: head.version, releasedKeys: [previous.key] } + } + + await supersedeVersionInTx(tx, head.id, now) + const version = head.version + 1 + await insertVersion(tx, params, version) + + /** Version numbers are dense per file, so a file cannot exceed the ceiling before this number. */ + const releasedKeys = + version > MAX_SUPERSEDED_FILE_VERSIONS + 1 + ? await pruneExcessWorkspaceFileVersionsInTx(tx, next.id) + : [] + return { version, releasedKeys } +} + +/** Inserts the new current version described by a write. */ +async function insertVersion( + tx: DbTransaction, + { next, nextProvenance, contentHash, write, now }: RecordWorkspaceFileVersionParams, + version: number +): Promise { + await tx.insert(workspaceFileVersion).values({ + id: generateId(), + fileId: next.id, + workspaceId: next.workspaceId as string, + version, + ...contentColumns(next, nextProvenance), + contentHash, + source: write.source, + authorUserIds: write.authorUserId ? [write.authorUserId] : [], + restoredFromVersion: write.restoredFromVersion ?? null, + createdAt: now, + updatedAt: now, + }) +} + +/** + * Deletes one superseded version, returning its storage key for release after commit, or null when + * no such superseded version exists. The current version is never deletable: it is the file. + */ +export async function deleteWorkspaceFileVersionInTx( + tx: DbTransaction, + fileId: string, + version: number +): Promise { + const [deleted] = await tx + .delete(workspaceFileVersion) + .where( + and( + eq(workspaceFileVersion.fileId, fileId), + eq(workspaceFileVersion.version, version), + isNotNull(workspaceFileVersion.supersededAt) + ) + ) + .returning({ key: workspaceFileVersion.key }) + return deleted?.key ?? null +} + +/** Deletes superseded versions beyond {@link MAX_SUPERSEDED_FILE_VERSIONS}, returning their keys. */ +async function pruneExcessWorkspaceFileVersionsInTx( + tx: DbTransaction, + fileId: string +): Promise { + const excess = await tx + .select({ id: workspaceFileVersion.id, key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where( + and(eq(workspaceFileVersion.fileId, fileId), isNotNull(workspaceFileVersion.supersededAt)) + ) + .orderBy(desc(workspaceFileVersion.version)) + .offset(MAX_SUPERSEDED_FILE_VERSIONS) + if (excess.length === 0) return [] + await tx.delete(workspaceFileVersion).where( + inArray( + workspaceFileVersion.id, + excess.map((row) => row.id) + ) + ) + return excess.map((row) => row.key) +} + +/** The file fields that identify its current bytes. */ +export type WorkspaceFileVersionSubject = Pick< + WorkspaceFileRecord, + 'id' | 'key' | 'size' | 'type' | 'uploadedBy' | 'uploadedAt' | 'updatedAt' | 'contentUpdatedAt' +> + +/** One version of a workspace file as readers see it. */ +export interface WorkspaceFileVersionRecord { + fileId: string + version: number + key: string + size: number + contentType: string + source: WorkspaceFileVersionSource + authorUserIds: string[] + restoredFromVersion: number | null + isCurrent: boolean + createdAt: Date + updatedAt: Date + supersededAt: Date | null + secretProvenance: WorkspaceFileSecretProvenanceSnapshot +} + +function toVersionRecord(row: WorkspaceFileVersionRow): WorkspaceFileVersionRecord { + return { + fileId: row.fileId, + version: row.version, + key: row.key, + size: row.sizeBytes, + contentType: row.contentType, + source: row.source, + authorUserIds: row.authorUserIds, + restoredFromVersion: row.restoredFromVersion, + isCurrent: row.supersededAt === null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + supersededAt: row.supersededAt, + secretProvenance: { + status: row.secretProvenanceStatus as WorkspaceFileSecretProvenanceSnapshot['status'], + entries: row.secretProvenanceEntries, + }, + } +} + +/** + * Version 1 of a file with no history yet — every file before its first content write. Attributed + * exactly as {@link recordWorkspaceFileVersionInTx} will materialize it, so the version a reader sees + * keeps its identity once it becomes a row. Its provenance is read live by callers that need it, + * since the sidecar still describes these bytes. + */ +function implicitFirstVersion(file: WorkspaceFileVersionSubject): WorkspaceFileVersionRecord { + const contentUpdatedAt = file.contentUpdatedAt ?? file.updatedAt + const original = isOriginalUploadContent({ uploadedAt: file.uploadedAt, contentUpdatedAt }) + return { + fileId: file.id, + version: 1, + key: file.key, + size: file.size, + contentType: file.type, + source: original ? 'upload' : 'unknown', + authorUserIds: original ? [file.uploadedBy] : [], + restoredFromVersion: null, + isCurrent: true, + createdAt: contentUpdatedAt, + updatedAt: contentUpdatedAt, + supersededAt: null, + secretProvenance: { status: null, entries: [] }, + } +} + +const VERSION_KEYSET: readonly KeysetKey<{ version: number }>[] = [ + numberKey(workspaceFileVersion.version, (row) => row.version), +] + +/** A page of a file's versions ordered by version number, resumable by keyset. */ +export async function queryWorkspaceFileVersions( + file: WorkspaceFileVersionSubject, + options: { sortOrder: ListSortOrder; limit: number; after?: CursorKey[] } +): Promise<{ versions: WorkspaceFileVersionRecord[]; nextKeys: CursorKey[] | null }> { + const resume = resumeKeyset(VERSION_KEYSET, options.after, options.sortOrder) + const rows = await db + .select() + .from(workspaceFileVersion) + .where(and(eq(workspaceFileVersion.fileId, file.id), resume)) + .orderBy(...listOrderBy(keysetColumns(VERSION_KEYSET), options.sortOrder)) + .limit(options.limit + 1) + const records = rows.map(toVersionRecord) + /** + * A file with no rows lists its implicit version 1, the only version it has. Any cursor was minted + * past that version already, so only the first page can carry it. + */ + if (records.length === 0 && !options.after && !(await loadWorkspaceFileVersionHead(file.id))) { + records.push(implicitFirstVersion(file)) + } + const page = keysetPage(VERSION_KEYSET, records, options.limit) + return { versions: page.data, nextKeys: page.nextCursorKeys } +} + +/** + * The subset of `keys` that name a retained version object. Such an object is reachable only + * through the version routes, which authorize against its file; key-addressed readers must refuse + * it rather than fall back to the object's own metadata or treat it as an untracked legacy file. + */ +export async function findWorkspaceFileVersionKeys(keys: readonly string[]): Promise> { + if (keys.length === 0) return new Set() + const rows = await db + .select({ key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where(inArray(workspaceFileVersion.key, [...keys])) + return new Set(rows.map((row) => row.key)) +} + +/** The current version, or the implicit version 1 of a file with no history rows. */ +export async function getCurrentWorkspaceFileVersion( + file: WorkspaceFileVersionSubject +): Promise { + const head = await loadWorkspaceFileVersionHead(file.id) + return head ? toVersionRecord(head) : implicitFirstVersion(file) +} + +/** One version of a file, or null when it never existed or retention removed it. */ +export async function getWorkspaceFileVersion( + file: WorkspaceFileVersionSubject, + version: number +): Promise { + const [row] = await db + .select() + .from(workspaceFileVersion) + .where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.version, version))) + .limit(1) + if (row) return toVersionRecord(row) + if (version !== 1 || (await loadWorkspaceFileVersionHead(file.id))) return null + return implicitFirstVersion(file) +} diff --git a/apps/sim/lib/uploads/core/errors.ts b/apps/sim/lib/uploads/core/errors.ts index d1c439a1a07..aed6e8d2844 100644 --- a/apps/sim/lib/uploads/core/errors.ts +++ b/apps/sim/lib/uploads/core/errors.ts @@ -45,3 +45,16 @@ export function isObjectNotFoundError(error: unknown): boolean { } return code === 404 || statusCode === 404 || $metadata?.httpStatusCode === 404 } + +/** + * True when `error`, or any error it wraps as its `cause`, reports a missing object. For callers + * that read an object through a layer which re-wraps storage failures but keeps the original. + */ +export function hasObjectNotFoundCause(error: unknown): boolean { + let current: unknown = error + for (let depth = 0; current && depth < 5; depth++) { + if (isObjectNotFoundError(current)) return true + current = (current as { cause?: unknown }).cause + } + return false +} diff --git a/apps/sim/lib/users/account-deletion.ts b/apps/sim/lib/users/account-deletion.ts index 1b81d20988c..b9876209500 100644 --- a/apps/sim/lib/users/account-deletion.ts +++ b/apps/sim/lib/users/account-deletion.ts @@ -11,6 +11,7 @@ import { user, workspaceFile, workspaceFiles, + workspaceFileVersion, workspace as workspaceTable, } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -491,6 +492,23 @@ async function collectAccountStorageKeys( (row) => (row.context as StorageContext | null) ?? 'workspace' ) + await collectPages( + (afterId) => + db + .select({ id: workspaceFileVersion.id, key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where( + and( + inArray(workspaceFileVersion.workspaceId, workspaceIds), + gt(workspaceFileVersion.id, afterId) + ) + ) + .orderBy(workspaceFileVersion.id) + .limit(STORAGE_PAGE_SIZE), + batches, + () => 'workspace' + ) + await collectPages( (afterId) => db diff --git a/apps/sim/lib/workspace-files/application/download-workspace-file.ts b/apps/sim/lib/workspace-files/application/download-workspace-file.ts index eadba577388..92a65339969 100644 --- a/apps/sim/lib/workspace-files/application/download-workspace-file.ts +++ b/apps/sim/lib/workspace-files/application/download-workspace-file.ts @@ -1,4 +1,5 @@ import { AuditAction, AuditResourceType } from '@sim/audit' +import type { Principal } from '@sim/auth/principal' import type { AuthorizedWorkspaceUseCaseContext } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream' @@ -72,11 +73,7 @@ export const downloadWorkspaceFile = defineAuthorizedWorkspaceFileUseCase({ function resolveRenderedArtifact( file: DownloadWorkspaceFileResult['file'], - filePrincipal: AuthorizedWorkspaceUseCaseContext< - typeof fileOperations.download, - DownloadWorkspaceFileInput, - ActiveWorkspaceFileContext - >['principal'] + filePrincipal: Principal ) { return resolveRenderedWorkspaceArtifact(file, filePrincipal, { maxBytes: MAX_RENDERED_DOCUMENT_BYTES, @@ -95,7 +92,17 @@ async function executeDownloadWorkspaceFileStream({ throwOnError: true, }) if (!file) throw new OrchestrationError('not_found', 'File not found') + return streamWorkspaceFileRecord(file, principal) +} +/** + * Streams the bytes a record points at. Version downloads pass a record whose key, size, and type + * describe a previous version, so both surfaces serve through one path. + */ +export async function streamWorkspaceFileRecord( + file: DownloadWorkspaceFileResult['file'], + principal: Principal +): Promise { /** * AI-generated docs store their generation SOURCE as the primary file and keep * the rendered binary in a separate artifact store, so streaming `file.key` diff --git a/apps/sim/lib/workspace-files/application/edit-workspace-file-content.ts b/apps/sim/lib/workspace-files/application/edit-workspace-file-content.ts index 3de5c7bfb2b..e7ec2680c8c 100644 --- a/apps/sim/lib/workspace-files/application/edit-workspace-file-content.ts +++ b/apps/sim/lib/workspace-files/application/edit-workspace-file-content.ts @@ -16,6 +16,7 @@ import { } from '@/lib/uploads/contexts/workspace' import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' +import { resolveWorkspaceFileVersionWrite } from '@/lib/workspace-files/application/file-version-write' import { fileOperations } from '@/lib/workspace-files/application/operations' import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' import { @@ -180,6 +181,7 @@ export const editWorkspaceFileContent = defineAuthorizedWorkspaceFileUseCase({ content, file.type, { + version: resolveWorkspaceFileVersionWrite(principal), expectedUpdatedAt: file.contentUpdatedAt, secretProvenancePolicy: input.secretProvenance ? { mode: 'replace' as const, provenance: input.secretProvenance } diff --git a/apps/sim/lib/workspace-files/application/file-version-write.ts b/apps/sim/lib/workspace-files/application/file-version-write.ts new file mode 100644 index 00000000000..7bea84114cc --- /dev/null +++ b/apps/sim/lib/workspace-files/application/file-version-write.ts @@ -0,0 +1,36 @@ +import { type Principal, resolvePrincipalAuditAttribution } from '@sim/auth/principal' +import type { WorkspaceFileVersionSource } from '@sim/db/schema' +import type { WorkspaceFileVersionWrite } from '@/lib/uploads/contexts/workspace/workspace-file-versions' + +function versionSourceForPrincipal(principal: Principal): WorkspaceFileVersionSource { + switch (principal.kind) { + case 'session': + return 'user' + case 'personal_api_key': + case 'oauth_access_token': + case 'workspace_api_key': + return 'api' + case 'delegated': + if (principal.serviceId === 'copilot') return 'copilot' + if (principal.serviceId === 'executor') return 'workflow' + if (principal.serviceId === 'realtime') return 'collab' + return 'unknown' + default: + return 'unknown' + } +} + +/** + * Records a content write under the acting user — the audit actor, never a billing owner — and + * the surface that made it. Actorless callers such as workspace API keys record no author. + */ +export function resolveWorkspaceFileVersionWrite( + principal: Principal, + overrides: Partial = {} +): WorkspaceFileVersionWrite { + return { + source: versionSourceForPrincipal(principal), + authorUserId: resolvePrincipalAuditAttribution(principal).actorId, + ...overrides, + } +} diff --git a/apps/sim/lib/workspace-files/application/file-versions.test.ts b/apps/sim/lib/workspace-files/application/file-versions.test.ts new file mode 100644 index 00000000000..d0811b7b3f6 --- /dev/null +++ b/apps/sim/lib/workspace-files/application/file-versions.test.ts @@ -0,0 +1,345 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + ContentVersionConflictError: class extends Error {}, + loadActive: vi.fn(), + getFile: vi.fn(), + fetchBuffer: vi.fn(), + updateContent: vi.fn(), + deleteStored: vi.fn(), + getVersion: vi.fn(), + getCurrentVersion: vi.fn(), + streamRecord: vi.fn(), + recordAudit: vi.fn(), + notify: vi.fn(), + resolvePermission: vi.fn(), +})) + +vi.mock('@sim/platform-authz/workspace', () => ({ + permissionSatisfies: () => true, + resolveEffectiveWorkspacePermission: mocks.resolvePermission, +})) + +vi.mock('@sim/audit', () => ({ + AuditAction: { + FILE_DOWNLOADED: 'FILE_DOWNLOADED', + FILE_REVERTED: 'FILE_REVERTED', + FILE_VERSION_DELETED: 'FILE_VERSION_DELETED', + }, + AuditResourceType: { FILE: 'FILE' }, + recordAudit: mocks.recordAudit, +})) +vi.mock('@/lib/realtime/notify', () => ({ notifyWorkspaceFilesChanged: mocks.notify })) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + loadActiveWorkspaceFileContext: mocks.loadActive, + loadWorkspaceFileLifecycleContext: vi.fn(), +})) +vi.mock('@/lib/uploads/contexts/workspace', () => ({ + ContentVersionConflictError: mocks.ContentVersionConflictError, + deleteWorkspaceFileVersion: mocks.deleteStored, + fetchWorkspaceFileBuffer: mocks.fetchBuffer, + getWorkspaceFile: mocks.getFile, + updateWorkspaceFileContent: mocks.updateContent, +})) +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + getCurrentWorkspaceFileVersion: mocks.getCurrentVersion, + getWorkspaceFileVersion: mocks.getVersion, + queryWorkspaceFileVersions: vi.fn(), +})) +vi.mock('@/lib/workspace-files/application/download-workspace-file', () => ({ + streamWorkspaceFileRecord: mocks.streamRecord, +})) +vi.mock('@/lib/workspace-files/application/read-workspace-file-text', () => ({ + extractWorkspaceFileRecordText: vi.fn(), +})) +vi.mock('@/lib/uploads', () => ({ getServePathPrefix: () => '/api/files/serve/' })) + +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { + deleteWorkspaceFileVersion, + downloadWorkspaceFileVersion, + revertWorkspaceFileVersion, +} from '@/lib/workspace-files/application/file-versions' + +const principal = { kind: 'session', userId: 'user-1', sessionId: 'session-1' } as const + +const context = { + fileId: 'file-1', + workspaceId: 'workspace-1', + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + billedAccountUserId: 'billing-owner', +} + +const file = { + id: 'file-1', + workspaceId: 'workspace-1', + name: 'notes.md', + key: 'workspace/workspace-1/3-current-notes.md', + path: '/api/files/serve/current?context=workspace', + size: 12, + type: 'text/markdown', + uploadedBy: 'user-1', + folderId: null, + folderPath: null, + deletedAt: null, + uploadedAt: new Date('2026-01-01T00:00:00Z'), + updatedAt: new Date('2026-01-03T00:00:00Z'), + contentUpdatedAt: new Date('2026-01-03T00:00:00Z'), +} + +function version(number: number, overrides: Record = {}) { + return { + fileId: 'file-1', + version: number, + key: `workspace/workspace-1/${number}-notes.md`, + size: 10, + contentType: 'text/markdown', + source: 'api', + authorUserIds: ['user-1'], + restoredFromVersion: null, + isCurrent: false, + createdAt: new Date('2026-01-02T00:00:00Z'), + updatedAt: new Date('2026-01-02T00:00:00Z'), + supersededAt: new Date('2026-01-03T00:00:00Z'), + secretProvenance: { status: 'exact', entries: [] }, + ...overrides, + } +} + +const current = version(3, { key: file.key, isCurrent: true, supersededAt: null }) + +function objectMissing() { + return Object.assign(new Error('Failed to download file: missing'), { + cause: Object.assign(new Error('missing'), { name: 'NoSuchKey' }), + }) +} + +describe('file version use cases', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.loadActive.mockResolvedValue(context) + mocks.resolvePermission.mockResolvedValue('write') + mocks.getFile.mockResolvedValue(file) + mocks.getCurrentVersion.mockResolvedValue(current) + mocks.getVersion.mockImplementation(async (_file: unknown, number: number) => + number === 3 ? current : number === 2 ? version(2) : null + ) + mocks.fetchBuffer.mockResolvedValue(Buffer.from('old content')) + mocks.updateContent.mockResolvedValue({ ...file, key: 'new-key', currentVersion: 4 }) + mocks.notify.mockResolvedValue(undefined) + }) + + describe('revertWorkspaceFileVersion', () => { + it('writes the version as a new revert version carrying its provenance snapshot', async () => { + mocks.getVersion.mockImplementation(async (_file: unknown, number: number) => + number === 2 + ? version(2) + : number === 4 + ? version(4, { source: 'revert', restoredFromVersion: 2, isCurrent: true }) + : current + ) + + const result = await revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + + expect(mocks.updateContent).toHaveBeenCalledWith( + 'workspace-1', + 'file-1', + 'user-1', + Buffer.from('old content'), + 'text/markdown', + { + version: { source: 'revert', authorUserId: 'user-1', restoredFromVersion: 2 }, + expectedUpdatedAt: file.contentUpdatedAt, + secretProvenancePolicy: { + mode: 'reinstate', + snapshot: { status: 'exact', entries: [] }, + }, + } + ) + expect(result).toMatchObject({ + reverted: true, + revertedFrom: 3, + revertedTo: 2, + version: { version: 4, restoredFromVersion: 2 }, + }) + expect(mocks.recordAudit).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'FILE_REVERTED', + metadata: expect.objectContaining({ + previousVersion: 3, + restoredVersion: 2, + newVersion: 4, + }), + }) + ) + expect(mocks.notify).toHaveBeenCalledWith('workspace-1') + }) + + it('does nothing, audits nothing, and notifies no one when the version is already current', async () => { + const result = await revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 3 }, + }) + + expect(result).toMatchObject({ reverted: false, version: { version: 3 } }) + expect(mocks.updateContent).not.toHaveBeenCalled() + expect(mocks.recordAudit).not.toHaveBeenCalled() + expect(mocks.notify).not.toHaveBeenCalled() + }) + + it('refuses a stale expected current version without writing', async () => { + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { + fileId: 'file-1', + assertedWorkspaceId: 'workspace-1', + version: 2, + expectedCurrentVersion: 2, + }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + expect(mocks.updateContent).not.toHaveBeenCalled() + }) + + it('maps a racing content write to a conflict', async () => { + mocks.updateContent.mockRejectedValueOnce(new mocks.ContentVersionConflictError('raced')) + + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + }) + + it('refuses a version above the buffered revert limit before reading it', async () => { + mocks.getVersion.mockResolvedValueOnce(version(2, { size: MAX_BUFFERED_TRANSFER_BYTES + 1 })) + + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'payload_too_large' }) + expect(mocks.fetchBuffer).not.toHaveBeenCalled() + }) + + it('answers 404 for a version that never existed or whose object is gone', async () => { + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 9 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + + mocks.fetchBuffer.mockRejectedValueOnce(objectMissing()) + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.updateContent).not.toHaveBeenCalled() + }) + + it('conceals a file asserted under another workspace', async () => { + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-2', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.getFile).not.toHaveBeenCalled() + }) + }) + + describe('downloadWorkspaceFileVersion', () => { + it('streams the version bytes under the file name and audits the version', async () => { + mocks.streamRecord.mockImplementation(async (record: { key: string }) => ({ + file: record, + stream: new ReadableStream(), + contentLength: 10, + contentType: 'text/markdown', + })) + + const result = await downloadWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + + expect(mocks.streamRecord).toHaveBeenCalledWith( + expect.objectContaining({ key: version(2).key, name: 'notes.md', url: undefined }), + principal + ) + expect(result.file).toBe(file) + expect(mocks.recordAudit).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'FILE_DOWNLOADED', + metadata: expect.objectContaining({ version: 2, bytes: 10 }), + }) + ) + }) + + it('answers 404 when the version object was removed mid-request', async () => { + mocks.streamRecord.mockRejectedValueOnce(objectMissing().cause) + + await expect( + downloadWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + }) + }) + + describe('deleteWorkspaceFileVersion', () => { + it('deletes a superseded version and audits it', async () => { + mocks.deleteStored.mockResolvedValueOnce(true) + + const result = await deleteWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + + expect(result).toEqual({ file, version: 2 }) + expect(mocks.deleteStored).toHaveBeenCalledWith('workspace-1', 'file-1', 2) + expect(mocks.recordAudit).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'FILE_VERSION_DELETED', + metadata: expect.objectContaining({ version: 2 }), + }) + ) + }) + + it('refuses to delete the current version', async () => { + await expect( + deleteWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 3 }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + expect(mocks.deleteStored).not.toHaveBeenCalled() + }) + + it('answers 404 when the version disappeared before the delete committed', async () => { + mocks.deleteStored.mockResolvedValueOnce(false) + + await expect( + deleteWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.recordAudit).not.toHaveBeenCalled() + }) + }) +}) diff --git a/apps/sim/lib/workspace-files/application/file-versions.ts b/apps/sim/lib/workspace-files/application/file-versions.ts new file mode 100644 index 00000000000..4a984af0c9e --- /dev/null +++ b/apps/sim/lib/workspace-files/application/file-versions.ts @@ -0,0 +1,377 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { type Principal, resolvePrincipalAttribution } from '@sim/auth/principal' +import { createLogger } from '@sim/logger' +import type { CursorKey, ListSortOrder } from '@/lib/api/list-query' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { notifyWorkspaceFilesChanged } from '@/lib/realtime/notify' +import { getServePathPrefix } from '@/lib/uploads' +import { + type ActiveWorkspaceFileContext, + ContentVersionConflictError, + deleteWorkspaceFileVersion as deleteStoredWorkspaceFileVersion, + fetchWorkspaceFileBuffer, + getWorkspaceFile, + updateWorkspaceFileContent, + type WorkspaceFileRecord, +} from '@/lib/uploads/contexts/workspace' +import { + getCurrentWorkspaceFileVersion, + getWorkspaceFileVersion, + queryWorkspaceFileVersions, + type WorkspaceFileVersionRecord, +} from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { hasObjectNotFoundCause } from '@/lib/uploads/core/errors' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { formatFileSize } from '@/lib/uploads/utils/file-utils' +import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' +import { + type DownloadWorkspaceFileStreamResult, + streamWorkspaceFileRecord, +} from '@/lib/workspace-files/application/download-workspace-file' +import { resolveWorkspaceFileVersionWrite } from '@/lib/workspace-files/application/file-version-write' +import { fileOperations } from '@/lib/workspace-files/application/operations' +import { + extractWorkspaceFileRecordText, + type ReadWorkspaceFileTextInput, + type ReadWorkspaceFileTextResult, +} from '@/lib/workspace-files/application/read-workspace-file-text' +import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' + +const logger = createLogger('WorkspaceFileVersions') + +interface FileVersionTarget { + fileId: string + assertedWorkspaceId?: string +} + +interface FileVersionRef extends FileVersionTarget { + version: number +} + +export interface ListWorkspaceFileVersionsInput extends FileVersionTarget { + sortOrder: ListSortOrder + limit: number + after?: CursorKey[] +} + +export interface ListWorkspaceFileVersionsResult { + file: WorkspaceFileRecord + versions: WorkspaceFileVersionRecord[] + nextKeys: CursorKey[] | null +} + +export interface ReadWorkspaceFileVersionResult { + file: WorkspaceFileRecord + version: WorkspaceFileVersionRecord +} + +export type ReadWorkspaceFileVersionTextInput = FileVersionRef & + Pick + +export interface ReadWorkspaceFileVersionTextResult extends ReadWorkspaceFileTextResult { + version: WorkspaceFileVersionRecord +} + +export interface DownloadWorkspaceFileVersionResult extends DownloadWorkspaceFileStreamResult { + version: WorkspaceFileVersionRecord +} + +export interface RevertWorkspaceFileVersionInput extends FileVersionRef { + /** When set, the revert commits only while this is still the file's current version. */ + expectedCurrentVersion?: number +} + +export interface RevertWorkspaceFileVersionResult { + file: WorkspaceFileRecord + version: WorkspaceFileVersionRecord + /** False when the requested version was already current and nothing was written. */ + reverted: boolean + /** The version that was current before the revert. */ + revertedFrom: number + /** The version whose bytes were requested. */ + revertedTo: number +} + +async function loadActiveFile(context: ActiveWorkspaceFileContext): Promise { + const file = await getWorkspaceFile(context.workspaceId, context.fileId, { throwOnError: true }) + if (!file) throw new OrchestrationError('not_found', 'File not found') + return file +} + +async function loadVersion( + file: WorkspaceFileRecord, + version: number +): Promise { + const record = await getWorkspaceFileVersion(file, version) + if (!record) throw new OrchestrationError('not_found', `Version ${version} not found`) + return record +} + +/** + * Runs a read of a version's stored object, answering 404 when the object is gone — retention or a + * delete can remove a superseded version between loading its row and reading its bytes. + */ +async function readVersionObject( + version: WorkspaceFileVersionRecord, + read: () => Promise +): Promise { + try { + return await read() + } catch (error) { + if (hasObjectNotFoundCause(error)) { + throw new OrchestrationError('not_found', `Version ${version.version} not found`) + } + throw error + } +} + +/** + * The file as it was at `version`: current name and location, that version's bytes. Rename and move + * are metadata writes that never create versions, so a version always reads under today's name. + */ +function recordAtVersion( + file: WorkspaceFileRecord, + version: WorkspaceFileVersionRecord +): WorkspaceFileRecord { + if (version.isCurrent) return file + return { + ...file, + key: version.key, + path: `${getServePathPrefix()}${encodeURIComponent(version.key)}?context=workspace`, + url: undefined, + size: version.size, + type: version.contentType, + } +} + +export const listWorkspaceFileVersions = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.listVersions, + resolveContext: ({ input }: { input: ListWorkspaceFileVersionsInput }) => + resolveActiveWorkspaceFileContext(input), + async execute({ input, context }): Promise { + const file = await loadActiveFile(context) + const { versions, nextKeys } = await queryWorkspaceFileVersions(file, { + sortOrder: input.sortOrder, + limit: input.limit, + after: input.after, + }) + return { file, versions, nextKeys } + }, +}) + +export const readWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.readVersion, + resolveContext: ({ input }: { input: FileVersionRef }) => + resolveActiveWorkspaceFileContext(input), + async execute({ input, context }): Promise { + const file = await loadActiveFile(context) + return { file, version: await loadVersion(file, input.version) } + }, +}) + +export const readWorkspaceFileVersionText = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.readVersionContent, + resolveContext: ({ input }: { input: ReadWorkspaceFileVersionTextInput }) => + resolveActiveWorkspaceFileContext(input), + async execute({ + input, + context, + principal, + request, + }): Promise { + const file = await loadActiveFile(context) + const version = await loadVersion(file, input.version) + const result = await readVersionObject(version, () => + extractWorkspaceFileRecordText( + recordAtVersion(file, version), + input, + principal, + request?.signal + ) + ) + return { ...result, file, version } + }, +}) + +export const downloadWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.downloadVersion, + resolveContext: ({ input }: { input: FileVersionRef }) => + resolveActiveWorkspaceFileContext(input), + async execute({ input, context, principal }): Promise { + const file = await loadActiveFile(context) + const version = await loadVersion(file, input.version) + const result = await readVersionObject(version, () => + streamWorkspaceFileRecord(recordAtVersion(file, version), principal) + ) + return { ...result, file, version } + }, + projectAudit: ({ result }) => ({ + action: AuditAction.FILE_DOWNLOADED, + resourceType: AuditResourceType.FILE, + resourceId: result.file.id, + resourceName: result.file.name, + description: `Downloaded version ${result.version.version} of file "${result.file.name}"`, + metadata: { + fileId: result.file.id, + fileName: result.file.name, + version: result.version.version, + bytes: result.contentLength, + }, + }), +}) + +async function executeRevertWorkspaceFileVersion({ + input, + context, + principal, +}: { + input: RevertWorkspaceFileVersionInput + context: ActiveWorkspaceFileContext + principal: Principal +}): Promise { + const file = await loadActiveFile(context) + const current = await getCurrentWorkspaceFileVersion(file) + if ( + input.expectedCurrentVersion !== undefined && + current.version !== input.expectedCurrentVersion + ) { + throw new OrchestrationError( + 'conflict', + `The current version is ${current.version}, not ${input.expectedCurrentVersion}` + ) + } + const target = await loadVersion(file, input.version) + if (target.isCurrent) { + return { + file, + version: target, + reverted: false, + revertedFrom: current.version, + revertedTo: target.version, + } + } + if (target.size > MAX_BUFFERED_TRANSFER_BYTES) { + throw new OrchestrationError( + 'payload_too_large', + `Version ${target.version} is ${formatFileSize(target.size)}, above the ${formatFileSize(MAX_BUFFERED_TRANSFER_BYTES)} revert limit` + ) + } + + const content = await readVersionObject(target, () => + fetchWorkspaceFileBuffer(recordAtVersion(file, target), { + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + }) + ) + const attribution = resolvePrincipalAttribution(principal, { + workspaceBillingOwnerUserId: context.billedAccountUserId, + }) + let updated: Awaited> + try { + updated = await updateWorkspaceFileContent( + context.workspaceId, + context.fileId, + attribution.attributedUserId, + content, + target.contentType, + { + version: resolveWorkspaceFileVersionWrite(principal, { + source: 'revert', + restoredFromVersion: target.version, + }), + expectedUpdatedAt: file.contentUpdatedAt ?? file.updatedAt, + secretProvenancePolicy: { mode: 'reinstate', snapshot: target.secretProvenance }, + } + ) + } catch (error) { + if (error instanceof ContentVersionConflictError) { + throw new OrchestrationError('conflict', error.message) + } + throw error + } + + logger.info('Reverted workspace file to a previous version', { + workspaceId: context.workspaceId, + fileId: context.fileId, + fromVersion: current.version, + toVersion: target.version, + }) + return { + file: updated, + version: + (await getWorkspaceFileVersion(updated, updated.currentVersion)) ?? + (await getCurrentWorkspaceFileVersion(updated)), + reverted: true, + revertedFrom: current.version, + revertedTo: target.version, + } +} + +/** + * Makes a previous version's bytes current again by writing them as a new version, so the revert is + * itself undoable. The bytes carry the provenance captured with that version, open editors merge + * the change, and a content write that races the revert fails it with a conflict. + */ +export const revertWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.revertVersion, + resolveContext: ({ input }: { input: RevertWorkspaceFileVersionInput }) => + resolveActiveWorkspaceFileContext(input), + execute: executeRevertWorkspaceFileVersion, + projectAudit: ({ result }) => + result.reverted + ? { + action: AuditAction.FILE_REVERTED, + resourceType: AuditResourceType.FILE, + resourceId: result.file.id, + resourceName: result.file.name, + description: `Reverted file "${result.file.name}" to version ${result.revertedTo}`, + metadata: { + previousVersion: result.revertedFrom, + restoredVersion: result.revertedTo, + newVersion: result.version.version, + }, + } + : [], + async afterSuccess({ context, result }) { + if (result.reverted) await notifyWorkspaceFilesChanged(context.workspaceId) + }, +}) + +export interface DeleteWorkspaceFileVersionResult { + file: WorkspaceFileRecord + version: number +} + +/** + * Permanently deletes one superseded version and its stored bytes — the way to purge content from + * history before retention would. The current version cannot be deleted: it is the file itself. + */ +export const deleteWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.deleteVersion, + resolveContext: ({ input }: { input: FileVersionRef }) => + resolveActiveWorkspaceFileContext(input), + async execute({ input, context }): Promise { + const file = await loadActiveFile(context) + const target = await loadVersion(file, input.version) + if (target.isCurrent) { + throw new OrchestrationError( + 'conflict', + `Version ${target.version} is the current version and cannot be deleted; revert to another version first` + ) + } + const deleted = await deleteStoredWorkspaceFileVersion( + context.workspaceId, + context.fileId, + target.version + ) + if (!deleted) throw new OrchestrationError('not_found', `Version ${target.version} not found`) + return { file, version: target.version } + }, + projectAudit: ({ result }) => ({ + action: AuditAction.FILE_VERSION_DELETED, + resourceType: AuditResourceType.FILE, + resourceId: result.file.id, + resourceName: result.file.name, + description: `Deleted version ${result.version} of file "${result.file.name}"`, + metadata: { version: result.version }, + }), +}) diff --git a/apps/sim/lib/workspace-files/application/operations.ts b/apps/sim/lib/workspace-files/application/operations.ts index 61e7c7252a7..4a4a5ac5263 100644 --- a/apps/sim/lib/workspace-files/application/operations.ts +++ b/apps/sim/lib/workspace-files/application/operations.ts @@ -24,7 +24,11 @@ const HUMAN_FILE_TOOL_PRINCIPAL_POLICY = { principalKinds: ['session', 'personal_api_key', 'oauth_access_token', 'delegated'], delegatedServices: ['copilot', 'executor'], } as const -const UPLOAD_PRINCIPAL_POLICY = { +/** + * Direct callers only. Uploads and version history admit no delegated service: no copilot or + * executor caller exists for them today, and admitting one is a separate decision. + */ +const DIRECT_PRINCIPAL_POLICY = { principalKinds: ['session', 'personal_api_key', 'oauth_access_token', 'workspace_api_key'], } as const @@ -176,6 +180,54 @@ export const fileOperations = { capability: 'files.use', ...ALL_COPILOT_PRINCIPAL_POLICY, }), + listVersions: defineWorkspaceOperation({ + id: 'files.versions.list', + oauthScope: 'api:read', + minimumRole: 'read', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), + readVersion: defineWorkspaceOperation({ + id: 'files.versions.read', + oauthScope: 'api:read', + minimumRole: 'read', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), + readVersionContent: defineWorkspaceOperation({ + id: 'files.versions.read_content', + oauthScope: 'api:read', + minimumRole: 'read', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), + downloadVersion: defineWorkspaceOperation({ + id: 'files.versions.download', + oauthScope: 'api:read', + minimumRole: 'read', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), + revertVersion: defineWorkspaceOperation({ + id: 'files.versions.revert', + oauthScope: 'api:write', + minimumRole: 'write', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), + deleteVersion: defineWorkspaceOperation({ + id: 'files.versions.delete', + oauthScope: 'api:write', + minimumRole: 'write', + workspaceApiKey: 'allow', + capability: 'files.use', + ...DIRECT_PRINCIPAL_POLICY, + }), readShare: defineWorkspaceOperation({ id: 'files.share.read', oauthScope: 'api:read', @@ -238,7 +290,7 @@ export const fileOperations = { minimumRole: 'write', workspaceApiKey: 'allow', capability: 'files.use', - ...UPLOAD_PRINCIPAL_POLICY, + ...DIRECT_PRINCIPAL_POLICY, }), /** * Reading an upload session's current state. Distinct from `uploadCancel`, @@ -252,7 +304,7 @@ export const fileOperations = { minimumRole: 'read', workspaceApiKey: 'allow', capability: 'files.use', - ...UPLOAD_PRINCIPAL_POLICY, + ...DIRECT_PRINCIPAL_POLICY, }), uploadParts: defineWorkspaceOperation({ id: 'files.upload.parts', @@ -260,7 +312,7 @@ export const fileOperations = { minimumRole: 'write', workspaceApiKey: 'allow', capability: 'files.use', - ...UPLOAD_PRINCIPAL_POLICY, + ...DIRECT_PRINCIPAL_POLICY, }), uploadComplete: defineWorkspaceOperation({ id: 'files.upload.complete', @@ -268,7 +320,7 @@ export const fileOperations = { minimumRole: 'write', workspaceApiKey: 'allow', capability: 'files.use', - ...UPLOAD_PRINCIPAL_POLICY, + ...DIRECT_PRINCIPAL_POLICY, }), uploadCancel: defineWorkspaceOperation({ id: 'files.upload.cancel', @@ -276,7 +328,7 @@ export const fileOperations = { minimumRole: 'write', workspaceApiKey: 'allow', capability: 'files.use', - ...UPLOAD_PRINCIPAL_POLICY, + ...DIRECT_PRINCIPAL_POLICY, }), } as const diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 621d20f1324..250abb3c628 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -7,6 +7,7 @@ import { getWorkspaceFile, type WorkspaceFileRecord, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { getCurrentWorkspaceFileVersion } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' import { fileOperations } from '@/lib/workspace-files/application/operations' import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' @@ -28,6 +29,11 @@ export interface ReadWorkspaceFileMetadataResult { share: ShareRecord | null } +export interface ReadWorkspaceFileMetadataWithVersionResult + extends ReadWorkspaceFileMetadataResult { + currentVersion: number +} + async function executeReadWorkspaceFileMetadata({ input, context, @@ -52,3 +58,18 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ resolveContext: ({ input }) => resolveActiveWorkspaceFileContext(input), execute: executeReadWorkspaceFileMetadata, }) + +/** + * The same read plus the current version number, for the public metadata surface. Kept separate so + * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. + */ +export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({ + operation: fileOperations.readMetadata, + resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) => + resolveActiveWorkspaceFileContext(input), + async execute(args): Promise { + const result = await executeReadWorkspaceFileMetadata(args) + const current = await getCurrentWorkspaceFileVersion(result.file) + return { ...result, currentVersion: current.version } + }, +}) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts index 0eb120bad07..fd3f29bcfc1 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-text.ts @@ -1,3 +1,4 @@ +import type { Principal } from '@sim/auth/principal' import { getErrorMessage } from '@sim/utils/errors' import type { AuthorizedWorkspaceUseCaseContext } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -97,7 +98,19 @@ async function executeReadWorkspaceFileText({ const file = await getWorkspaceFile(context.workspaceId, context.fileId, { throwOnError: true }) signal?.throwIfAborted() if (!file) throw new OrchestrationError('not_found', 'File not found') + return extractWorkspaceFileRecordText(file, input, principal, signal) +} +/** + * Extracts the text of the bytes a record points at. Version reads pass a record whose key, size, + * and type describe a previous version, so both surfaces extract through one path. + */ +export async function extractWorkspaceFileRecordText( + file: WorkspaceFileRecord, + input: Pick, + principal: Principal, + signal?: AbortSignal +): Promise { const extension = getFileExtension(file.name) if (!isSupportedFileType(extension)) { throw new OrchestrationError( diff --git a/apps/sim/lib/workspace-files/application/update-workspace-file-content.ts b/apps/sim/lib/workspace-files/application/update-workspace-file-content.ts index 56d096c7a2a..a201adb08bb 100644 --- a/apps/sim/lib/workspace-files/application/update-workspace-file-content.ts +++ b/apps/sim/lib/workspace-files/application/update-workspace-file-content.ts @@ -12,6 +12,7 @@ import { type WorkspaceFileSecretProvenance, } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' +import { resolveWorkspaceFileVersionWrite } from '@/lib/workspace-files/application/file-version-write' import { fileOperations } from '@/lib/workspace-files/application/operations' import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' import { MAX_WORKSPACE_FILE_CONTENT_BYTES } from '@/lib/workspace-files/orchestration' @@ -62,6 +63,7 @@ async function updateAuthorizedWorkspaceFileContent({ content, input.contentType, { + version: resolveWorkspaceFileVersionWrite(principal), ...(input.expectedUpdatedAt ? { expectedUpdatedAt: input.expectedUpdatedAt } : {}), syncLiveDoc: input.syncLiveDoc, secretProvenancePolicy: { diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 7d0680d3827..c1a917089ef 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -96,6 +96,10 @@ export const AuditAction = { FILE_SHARED: 'file.shared', FILE_SHARE_DISABLED: 'file.share_disabled', FILE_DOWNLOADED: 'file.downloaded', + /** A file's content was set back to one of its previous versions. */ + FILE_REVERTED: 'file.reverted', + /** A superseded version of a file's content was permanently deleted. */ + FILE_VERSION_DELETED: 'file.version_deleted', // Folders FOLDER_CREATED: 'folder.created', diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 08f0db6bf82..542d993db80 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1556,6 +1556,7 @@ export interface RetentionOverride { logRetentionHours?: number | null softDeleteRetentionHours?: number | null taskCleanupHours?: number | null + fileVersionRetentionHours?: number | null } /** @@ -1568,6 +1569,8 @@ export interface DataRetentionSettings { logRetentionHours?: number | null softDeleteRetentionHours?: number | null taskCleanupHours?: number | null + /** How long a superseded workspace file version is kept, measured from when it was superseded. */ + fileVersionRetentionHours?: number | null /** Enterprise PII redaction rules applied to workflow logs on persist. */ piiRedaction?: { rules?: PiiRedactionRule[] @@ -2711,6 +2714,85 @@ export const workspaceFileCollabState = pgTable('workspace_file_collab_state', { updatedAt: timestamp('updated_at').notNull().defaultNow(), }) +/** Where a workspace file version's bytes came from. */ +export const workspaceFileVersionSourceEnum = pgEnum('workspace_file_version_source', [ + 'upload', + 'user', + 'api', + 'copilot', + 'workflow', + 'collab', + 'revert', + 'unknown', +]) + +export type WorkspaceFileVersionSource = (typeof workspaceFileVersionSourceEnum.enumValues)[number] + +/** + * One content state of a workspace file. Rows cover every state since versioning began, including + * the current one (the row whose `key` equals `workspace_files.key`); a file with no rows has an + * implicit version 1 that the first content write materializes. Each row owns an immutable storage + * object, so a key referenced here must never be deleted while the row exists. + * + * `supersededAt` is NULL only for the current version and is the age retention measures from. + * `secretProvenanceStatus` NULL means the bytes predate provenance tracking (reads as exact-empty, + * like `workspace_files.secret_provenance_version` NULL); otherwise the status and entries are a + * snapshot of the sidecar for exactly these bytes, which a revert must reinstate. + */ +export const workspaceFileVersion = pgTable( + 'workspace_file_version', + { + id: text('id').primaryKey(), + fileId: text('file_id') + .notNull() + .references(() => workspaceFiles.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + version: integer('version').notNull(), + key: text('key').notNull(), + sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(), + contentType: text('content_type').notNull(), + /** sha256 (hex) of the bytes; NULL for an implicit version materialized without reading them. */ + contentHash: text('content_hash'), + /** The file's `contentUpdatedAt` while these bytes were current (millisecond precision). */ + contentUpdatedAt: timestamp('content_updated_at').notNull(), + supersededAt: timestamp('superseded_at'), + source: workspaceFileVersionSourceEnum('source').notNull(), + /** Users who wrote these bytes, in first-contribution order; empty when unattributable. */ + authorUserIds: text('author_user_ids').array().notNull().default(sql`'{}'::text[]`), + restoredFromVersion: integer('restored_from_version'), + secretProvenanceStatus: text('secret_provenance_status'), + secretProvenanceEntries: jsonb('secret_provenance_entries') + .$type() + .notNull() + .default([]), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + fileVersionUnique: uniqueIndex('workspace_file_version_file_version_unique').on( + table.fileId, + table.version + ), + keyUnique: uniqueIndex('workspace_file_version_key_unique').on(table.key), + /** Serves account deletion's keyset walk over every version in a set of workspaces. */ + workspaceIdIdx: index('workspace_file_version_workspace_id_idx').on( + table.workspaceId, + table.id + ), + supersededIdx: index('workspace_file_version_workspace_superseded_idx') + .on(table.workspaceId, table.supersededAt) + .where(sql`${table.supersededAt} IS NOT NULL`), + provenanceStatusCheck: check( + 'workspace_file_version_provenance_status_check', + sql`${table.secretProvenanceStatus} IS NULL OR ${table.secretProvenanceStatus} IN ('exact', 'unknown', 'unrecorded')` + ), + }) +) + +export type WorkspaceFileVersionRow = typeof workspaceFileVersion.$inferSelect + /** * Public share links for workspace resources. Polymorphic on `resourceType` so a * single mechanism serves files now and folders later. One row per resource diff --git a/packages/sim-cli/src/commands/protocol/files-get.ts b/packages/sim-cli/src/commands/protocol/files-get.ts index 6392c66c7b9..fff79f450fb 100644 --- a/packages/sim-cli/src/commands/protocol/files-get.ts +++ b/packages/sim-cli/src/commands/protocol/files-get.ts @@ -245,6 +245,61 @@ export function isTerminalSafeContentType(contentType: string | null): boolean { ) } +interface DownloadOutputOptions { + outputFile?: string + force?: boolean +} + +type DownloadOperation = (typeof V2_OPERATIONS)['downloadFile' | 'downloadFileVersion'] + +/** + * Streams a binary v2 download to stdout or atomically to `--output-file`. Shared by every + * command that downloads file bytes, so each gets the same terminal guard and overwrite rules. + */ +async function downloadToOutput( + command: Command, + operation: DownloadOperation, + pathParams: Record, + options: DownloadOutputOptions +): Promise { + const target = options.outputFile + const writesToStdout = target === undefined || target === '-' + if (writesToStdout && options.force) { + throw new SimApiError('--force requires --output-file ', 0) + } + + const { client, profile } = clientFrom(command) + const workspaceId = client.requireWorkspace() + const response = await client.requestRaw(resolvePath(operation.path, pathParams), { + method: operation.method, + query: { workspaceId }, + }) + if (!response.body) { + throw new SimApiError('File content response was empty.', response.status) + } + + if (writesToStdout) { + const contentType = response.headers.get('content-type') + if (process.stdout.isTTY && !isTerminalSafeContentType(contentType)) { + await response.body.cancel() + throw new SimApiError( + `Refusing to write ${contentType ?? 'unknown content'} to an interactive terminal. Use --output-file or pipe stdout.`, + 0 + ) + } + + await streamToStdout(response.body) + return + } + + await saveToFile(response.body, target, Boolean(options.force)) + printProtocolResult(profile.output, { + id: pathParams.fileId, + path: target, + status: 'saved', + }) +} + export function attachFileGet(files: Command): void { files .command('get') @@ -253,50 +308,21 @@ export function attachFileGet(files: Command): void { .description('Get a file’s content') .option('-o, --output-file ', 'Write content to a file instead of stdout') .option('--force', 'Overwrite --output-file if it already exists') - .action( - async ( - fileId: string, - options: { outputFile?: string; force?: boolean }, - command: Command - ) => { - const writesToStdout = options.outputFile === undefined || options.outputFile === '-' - if (writesToStdout && options.force) { - throw new SimApiError('--force requires --output-file ', 0) - } - - const { client, profile } = clientFrom(command) - const workspaceId = client.requireWorkspace() - const operation = V2_OPERATIONS.downloadFile - const response = await client.requestRaw(resolvePath(operation.path, { fileId }), { - method: operation.method, - query: { workspaceId }, - }) - if (!response.body) { - throw new SimApiError('File content response was empty.', response.status) - } - - if (options.outputFile === undefined || options.outputFile === '-') { - const contentType = response.headers.get('content-type') - if (process.stdout.isTTY && !isTerminalSafeContentType(contentType)) { - await response.body.cancel() - throw new SimApiError( - `Refusing to write ${contentType ?? 'unknown content'} to an interactive terminal. Use --output-file or pipe stdout.`, - 0 - ) - } - - await streamToStdout(response.body) - return - } - - const target = options.outputFile + .action((fileId: string, options: DownloadOutputOptions, command: Command) => + downloadToOutput(command, V2_OPERATIONS.downloadFile, { fileId }, options) + ) +} - await saveToFile(response.body, target, Boolean(options.force)) - printProtocolResult(profile.output, { - id: fileId, - path: target, - status: 'saved', - }) - } +export function attachFileVersionDownload(versions: Command): void { + versions + .command('download') + .argument('', 'File identifier.') + .argument('', 'Version number.') + .allowExcessArguments(false) + .description('Download the content of one version of a file') + .option('-o, --output-file ', 'Write content to a file instead of stdout') + .option('--force', 'Overwrite --output-file if it already exists') + .action((fileId: string, version: string, options: DownloadOutputOptions, command: Command) => + downloadToOutput(command, V2_OPERATIONS.downloadFileVersion, { fileId, version }, options) ) } diff --git a/packages/sim-cli/src/commands/protocol/index.ts b/packages/sim-cli/src/commands/protocol/index.ts index 408843777d5..d9123d10fc5 100644 --- a/packages/sim-cli/src/commands/protocol/index.ts +++ b/packages/sim-cli/src/commands/protocol/index.ts @@ -1,6 +1,6 @@ import { Command } from 'commander' import { attachChat } from './chat' -import { attachFileGet } from './files-get' +import { attachFileGet, attachFileVersionDownload } from './files-get' import { attachFileUpload } from './files-upload' import { attachKnowledgeDocumentUpload } from './knowledge-document-upload' import { attachKnowledgeExport } from './knowledge-export' @@ -24,6 +24,7 @@ export function attachProtocolCommands(program: Command): void { const files = group(program, 'files') attachFileUpload(files) attachFileGet(files) + attachFileVersionDownload(group(files, 'versions')) attachResourceDirectoryCommands(files, { kind: 'file', resources: 'listFiles', diff --git a/packages/sim-cli/src/contract/commands.ts b/packages/sim-cli/src/contract/commands.ts index 5223eedf095..3db808390e2 100644 --- a/packages/sim-cli/src/contract/commands.ts +++ b/packages/sim-cli/src/contract/commands.ts @@ -1088,6 +1088,7 @@ export const CLI_CONTRACT: CliContract = { { header: 'uploaded by', path: 'uploadedByEmail' }, { header: 'uploaded', path: 'uploadedAt', format: 'timestamp' }, { header: 'updated', path: 'updatedAt', format: 'timestamp' }, + { header: 'version', path: 'currentVersion' }, // v2 returns the share under `share` (null when unshared), and its flag // is `isActive`. { header: 'shared', path: 'share.isActive', format: 'bool' }, @@ -1117,6 +1118,57 @@ export const CLI_CONTRACT: CliContract = { renamedFrom: ['files restore create'], describe: 'Restore an archived file', }, + /** + * Version history. The deriver files `/versions/[version]/text` under a `text` + * group and `/revert` under a lone `create`; both belong beside the list. The + * single-version read is `describe`, matching `files describe`, because `get` + * already means downloading content one level up (`files get`, and the + * hand-written `files versions download`). + */ + listFileVersions: { + command: 'files versions list', + describe: 'List the recorded versions of a file', + columns: [ + { header: 'version' }, + { header: 'current', path: 'isCurrent', format: 'bool' }, + { header: 'source' }, + { header: 'size', format: 'bytes' }, + { header: 'authors', format: 'count' }, + { header: 'created', path: 'createdAt', format: 'timestamp' }, + { header: 'superseded', path: 'supersededAt', format: 'timestamp' }, + ], + }, + getFileVersion: { + command: 'files versions describe', + describe: 'Show the metadata of one version of a file', + fields: [ + { header: 'file', path: 'fileId' }, + { header: 'version' }, + { header: 'current', path: 'isCurrent', format: 'bool' }, + { header: 'source' }, + { header: 'restored from', path: 'restoredFromVersion' }, + { header: 'size', format: 'bytes' }, + { header: 'type', path: 'contentType' }, + { header: 'authors', format: 'count' }, + { header: 'created', path: 'createdAt', format: 'timestamp' }, + { header: 'updated', path: 'updatedAt', format: 'timestamp' }, + { header: 'superseded', path: 'supersededAt', format: 'timestamp' }, + ], + }, + readFileVersionText: { + command: 'files versions read', + describe: 'Read the text content of one version of a file', + }, + /** No confirm: a revert writes the old content as a new version, so what it replaces stays revertible. */ + revertFileVersion: { + command: 'files versions revert', + describe: 'Make a previous version of a file current again', + }, + deleteFileVersion: { + command: 'files versions delete', + describe: 'Permanently delete a previous version of a file', + confirm: 'This permanently deletes the version and its stored content.', + }, // Left to derive, the folder restore lands under `files restore` and turns // that leaf back into a group holding a lone `create` — the exact shape the // rename above exists to remove. It belongs beside the other folder verbs. diff --git a/packages/sim-cli/src/generated/v2-api.ts b/packages/sim-cli/src/generated/v2-api.ts index 0b8bc8b0b9e..28465d201d2 100644 --- a/packages/sim-cli/src/generated/v2-api.ts +++ b/packages/sim-cli/src/generated/v2-api.ts @@ -2801,6 +2801,26 @@ export type DeleteFileFolderResponse = { data: DeleteFileFolderResponseRef0 } +/** `DELETE /api/v2/files/[fileId]/versions/[version]` */ +export type DeleteFileVersionParams = { + fileId: string + version: number +} + +export type DeleteFileVersionQuery = { + workspaceId: string +} + +type DeleteFileVersionResponseRef0 = { + fileId: string + version: number + deleted: true +} + +export type DeleteFileVersionResponse = { + data: DeleteFileVersionResponseRef0 +} + /** `DELETE /api/v2/knowledge/[knowledgeBaseId]` */ export type DeleteKnowledgeBaseParams = { knowledgeBaseId: string @@ -3459,6 +3479,19 @@ export type DownloadFileQuery = { /** Non-JSON response (`binary`). */ export type DownloadFileResponse = never +/** `GET /api/v2/files/[fileId]/versions/[version]/content` */ +export type DownloadFileVersionParams = { + fileId: string + version: number +} + +export type DownloadFileVersionQuery = { + workspaceId: string +} + +/** Non-JSON response (`binary`). */ +export type DownloadFileVersionResponse = never + /** `GET /api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]` */ export type DownloadRunFileParams = { workflowId: string @@ -4129,6 +4162,7 @@ type GetFileResponseRef1 = { updatedAt: string deletedAt: string | null share: GetFileResponseRef0 | null + currentVersion: number } export type GetFileResponse = { @@ -4210,6 +4244,37 @@ export type GetFileUploadResponse = { data: GetFileUploadResponseRef1 } +/** `GET /api/v2/files/[fileId]/versions/[version]` */ +export type GetFileVersionParams = { + fileId: string + version: number +} + +export type GetFileVersionQuery = { + workspaceId: string +} + +type GetFileVersionResponseRef0 = { + fileId: string + version: number + isCurrent: boolean + size: number + contentType: string + source: 'upload' | 'user' | 'api' | 'copilot' | 'workflow' | 'collab' | 'revert' | 'unknown' + authors: Array<{ + id: string + email: string | null + }> + restoredFromVersion: number | null + createdAt: string + updatedAt: string + supersededAt: string | null +} + +export type GetFileVersionResponse = { + data: GetFileVersionResponseRef0 +} + /** `GET /api/v2/knowledge/[knowledgeBaseId]` */ export type GetKnowledgeBaseParams = { knowledgeBaseId: string @@ -6217,6 +6282,41 @@ export type ListFilesResponse = { nextCursor: string | null } +/** `GET /api/v2/files/[fileId]/versions` */ +export type ListFileVersionsParams = { + fileId: string +} + +export type ListFileVersionsQuery = { + workspaceId: string + sortBy?: 'version' + sortOrder?: 'asc' | 'desc' + limit?: number + cursor?: string +} + +type ListFileVersionsResponseRef0 = { + fileId: string + version: number + isCurrent: boolean + size: number + contentType: string + source: 'upload' | 'user' | 'api' | 'copilot' | 'workflow' | 'collab' | 'revert' | 'unknown' + authors: Array<{ + id: string + email: string | null + }> + restoredFromVersion: number | null + createdAt: string + updatedAt: string + supersededAt: string | null +} + +export type ListFileVersionsResponse = { + data: Array + nextCursor: string | null +} + /** `GET /api/v2/knowledge` */ type ListKnowledgeBasesQueryRef0 = string @@ -8528,6 +8628,42 @@ export type ReadFileTextResponse = { data: ReadFileTextResponseRef0 } +/** `GET /api/v2/files/[fileId]/versions/[version]/text` */ +export type ReadFileVersionTextParams = { + fileId: string + version: number +} + +export type ReadFileVersionTextQuery = { + workspaceId: string + maxBytes?: number + offset?: number + limit?: number +} + +type ReadFileVersionTextResponseRef0 = { + fileId: string + name: string + type: string + text: string + truncated: boolean + degraded: boolean + degradedReason: string | null + charCount: number + byteCount: number + lineRange?: { + offset: number + lineCount: number + totalLines: number + totalLinesExact: boolean + } + version: number +} + +export type ReadFileVersionTextResponse = { + data: ReadFileVersionTextResponseRef0 +} + /** `PATCH /api/v2/files/folders` */ export type RelocateFileFolderQuery = Record @@ -9167,6 +9303,60 @@ export type ResumeWorkflowResponse = data: ResumeWorkflowResponseRef2 } +/** `POST /api/v2/files/[fileId]/versions/[version]/revert` */ +export type RevertFileVersionParams = { + fileId: string + version: number +} + +export type RevertFileVersionQuery = Record + +export type RevertFileVersionBody = { + workspaceId: string + expectedCurrentVersion?: number +} + +type RevertFileVersionResponseRef0 = { + id: string + webUrl: string + name: string + size: number + type: string + key: string + folderPath: string + uploadedByEmail: string + uploadedAt: string + updatedAt: string + deletedAt: string | null +} + +type RevertFileVersionResponseRef1 = { + fileId: string + version: number + isCurrent: boolean + size: number + contentType: string + source: 'upload' | 'user' | 'api' | 'copilot' | 'workflow' | 'collab' | 'revert' | 'unknown' + authors: Array<{ + id: string + email: string | null + }> + restoredFromVersion: number | null + createdAt: string + updatedAt: string + supersededAt: string | null +} + +type RevertFileVersionResponseRef2 = { + reverted: boolean + file: RevertFileVersionResponseRef0 + version: RevertFileVersionResponseRef1 +} + +export type RevertFileVersionResponse = { + data: RevertFileVersionResponseRef2 +} + /** `POST /api/v2/workflows/[workflowId]/versions/[version]/revert` */ export type RevertWorkflowVersionParams = { version: number | 'active' @@ -12308,6 +12498,17 @@ export const V2_OPERATIONS = { }, }, }, + deleteFileVersion: { + method: 'DELETE', + path: '/api/v2/files/[fileId]/versions/[version]', + pathParams: ['fileId', 'version'] as const, + pathParamDocs: { fileId: 'File identifier.', version: 'Version number.' }, + responseMode: 'json', + summary: 'Delete File Version', + query: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + }, + }, deleteKnowledgeBase: { method: 'DELETE', path: '/api/v2/knowledge/[knowledgeBaseId]', @@ -12741,6 +12942,17 @@ export const V2_OPERATIONS = { workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, }, }, + downloadFileVersion: { + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]/content', + pathParams: ['fileId', 'version'] as const, + pathParamDocs: { fileId: 'File identifier.', version: 'Version number.' }, + responseMode: 'binary', + summary: 'Download File Version', + query: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + }, + }, downloadRunFile: { method: 'GET', path: '/api/v2/workflows/[workflowId]/runs/[runId]/files/[fileId]', @@ -13074,6 +13286,17 @@ export const V2_OPERATIONS = { }, }, }, + getFileVersion: { + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]', + pathParams: ['fileId', 'version'] as const, + pathParamDocs: { fileId: 'File identifier.', version: 'Version number.' }, + responseMode: 'json', + summary: 'Get File Version', + query: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + }, + }, getKnowledgeBase: { method: 'GET', path: '/api/v2/knowledge/[knowledgeBaseId]', @@ -14239,6 +14462,40 @@ export const V2_OPERATIONS = { }, }, }, + listFileVersions: { + method: 'GET', + path: '/api/v2/files/[fileId]/versions', + pathParams: ['fileId'] as const, + pathParamDocs: { fileId: 'File identifier.' }, + responseMode: 'json', + summary: 'List File Versions', + query: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + sortBy: { + kind: 'enum', + values: ['version'] as const, + default: 'version', + describe: 'Field used to sort the result.', + }, + sortOrder: { + kind: 'enum', + values: ['asc', 'desc'] as const, + default: 'desc', + describe: 'Sort direction.', + }, + limit: { + kind: 'integer', + default: 50, + describe: + 'Maximum versions to return per page. Must be a whole number from 1 to 100. Defaults to 50.', + }, + cursor: { + kind: 'string', + describe: + 'Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.', + }, + }, + }, listKnowledgeBases: { method: 'GET', path: '/api/v2/knowledge', @@ -15972,6 +16229,30 @@ export const V2_OPERATIONS = { }, }, }, + readFileVersionText: { + method: 'GET', + path: '/api/v2/files/[fileId]/versions/[version]/text', + pathParams: ['fileId', 'version'] as const, + pathParamDocs: { fileId: 'File identifier.', version: 'Version number.' }, + responseMode: 'json', + summary: 'Read File Version Text', + query: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + maxBytes: { + kind: 'integer', + describe: + 'Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit.', + }, + offset: { + kind: 'integer', + describe: 'First line to return, 1-based. Absent starts at the first line.', + }, + limit: { + kind: 'integer', + describe: 'How many lines to return from `offset`. Absent reads to the end.', + }, + }, + }, relocateFileFolder: { method: 'PATCH', path: '/api/v2/files/folders', @@ -16243,6 +16524,22 @@ export const V2_OPERATIONS = { input: { kind: 'unknown', describe: 'Input supplied to the paused workflow block.' }, }, }, + revertFileVersion: { + method: 'POST', + path: '/api/v2/files/[fileId]/versions/[version]/revert', + pathParams: ['fileId', 'version'] as const, + pathParamDocs: { fileId: 'File identifier.', version: 'Version number.' }, + responseMode: 'json', + summary: 'Revert File Version', + body: { + workspaceId: { kind: 'string', required: true, describe: 'Workspace that owns the file.' }, + expectedCurrentVersion: { + kind: 'integer', + describe: + 'Revert only while this is still the current version; otherwise the request fails with `409`. Omit to revert whatever is current. Collaborative edits and repeated workflow writes that fold into the current version keep its number.', + }, + }, + }, revertWorkflowVersion: { method: 'POST', path: '/api/v2/workflows/[workflowId]/versions/[version]/revert', diff --git a/packages/sim-cli/src/http/client.test.ts b/packages/sim-cli/src/http/client.test.ts index bbcd16db43b..25b4c07989a 100644 --- a/packages/sim-cli/src/http/client.test.ts +++ b/packages/sim-cli/src/http/client.test.ts @@ -1104,6 +1104,8 @@ describe('destructive operations are gated', () => { 'restoreTableFolder', 'restoreWorkflow', 'resumeWorkflow', + // Writes the old content as a new version, so what it replaces stays revertible. + 'revertFileVersion', 'rollbackWorkflow', 'runRowEnrichment', 'createTableDispatch', diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 369e2fefd2f..783b4e85fbc 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -80,6 +80,8 @@ export const auditMock = { FILE_UPDATED: 'file.updated', FILE_DELETED: 'file.deleted', FILE_RESTORED: 'file.restored', + FILE_REVERTED: 'file.reverted', + FILE_VERSION_DELETED: 'file.version_deleted', FILE_MOVED: 'file.moved', FILE_SHARED: 'file.shared', FILE_SHARE_DISABLED: 'file.share_disabled', diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index f162dc4e5ae..19d23ce0cdf 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -761,6 +761,25 @@ export const schemaMock = { content: 'chunk.content', }, workspaceFileSearchSegment: workspaceFileSearchSegmentMock, + workspaceFileVersion: { + id: 'workspaceFileVersion.id', + fileId: 'workspaceFileVersion.fileId', + workspaceId: 'workspaceFileVersion.workspaceId', + version: 'workspaceFileVersion.version', + key: 'workspaceFileVersion.key', + sizeBytes: 'workspaceFileVersion.sizeBytes', + contentType: 'workspaceFileVersion.contentType', + contentHash: 'workspaceFileVersion.contentHash', + contentUpdatedAt: 'workspaceFileVersion.contentUpdatedAt', + supersededAt: 'workspaceFileVersion.supersededAt', + source: 'workspaceFileVersion.source', + authorUserIds: 'workspaceFileVersion.authorUserIds', + restoredFromVersion: 'workspaceFileVersion.restoredFromVersion', + secretProvenanceStatus: 'workspaceFileVersion.secretProvenanceStatus', + secretProvenanceEntries: 'workspaceFileVersion.secretProvenanceEntries', + createdAt: 'workspaceFileVersion.createdAt', + updatedAt: 'workspaceFileVersion.updatedAt', + }, workspaceFileSecretProvenance: { fileId: 'workspaceFileSecretProvenance.fileId', contentUpdatedAt: 'workspaceFileSecretProvenance.contentUpdatedAt', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 08630a0fc1c..1d75e87ed76 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -105,6 +105,7 @@ const INDIRECT_ZOD_ROUTES = new Set([ 'apps/sim/app/api/settings/allowed-mcp-domains/route.ts', 'apps/sim/app/api/cron/scim-reconcile/route.ts', 'apps/sim/app/api/cron/cleanup-tasks/route.ts', + 'apps/sim/app/api/cron/cleanup-file-versions/route.ts', 'apps/sim/app/api/cron/cleanup-soft-deletes/route.ts', 'apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts', 'apps/sim/app/api/cron/cleanup-stale-executions/route.ts', diff --git a/scripts/openapi/documents.test.ts b/scripts/openapi/documents.test.ts index a8f42412331..8bcd21afa02 100644 --- a/scripts/openapi/documents.test.ts +++ b/scripts/openapi/documents.test.ts @@ -110,7 +110,7 @@ async function routeApplicationOperation(routePath: string, method: string): Pro const EXPECTED_OPERATION_COUNTS = new Map([ ['apps/docs/openapi-v2-workflows.json', 58], ['apps/docs/openapi-v2-logs.json', 3], - ['apps/docs/openapi-v2-files-audit.json', 29], + ['apps/docs/openapi-v2-files-audit.json', 35], ['apps/docs/openapi-v2-tables.json', 53], ['apps/docs/openapi-v2-knowledge.json', 45], ['apps/docs/openapi-v2-billing.json', 2], @@ -310,7 +310,7 @@ describe('generated OpenAPI documents', () => { }) } } - expect(totalOperations).toBe(241) + expect(totalOperations).toBe(247) }) it('documents mixed workflow execution and resume responses', () => { From 35ac722115d66f527594bc95f55c25ac3aac1734 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 18:13:57 -0700 Subject: [PATCH 02/10] fix(files): address review feedback on file version history --- apps/docs/lib/openapi-download.test.ts | 2 +- apps/docs/openapi-v2-files-audit.json | 25 +++++----- apps/sim/app/api/v2/files/utils.test.ts | 50 +++++++++++++++++++ apps/sim/app/api/v2/files/utils.ts | 8 ++- apps/sim/background/cleanup-file-versions.ts | 14 ++---- apps/sim/background/cleanup-soft-deletes.ts | 27 ++++++++-- .../sim/lib/api/contracts/v2/file-versions.ts | 26 +++++----- apps/sim/lib/api/contracts/v2/files.ts | 11 ++-- .../api/contracts/v2/openapi/files-audit.ts | 2 +- .../lib/api/mcp/generated/v2-operations.ts | 2 +- .../file-versions.integration.ts | 4 +- apps/sim/lib/users/queries.ts | 19 +++++++ .../read-workspace-file-metadata.test.ts | 37 +++++++++++++- .../read-workspace-file-metadata.ts | 17 +++++-- 14 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 apps/sim/app/api/v2/files/utils.test.ts diff --git a/apps/docs/lib/openapi-download.test.ts b/apps/docs/lib/openapi-download.test.ts index 7a2bcdb26f9..5fccfe6ce59 100644 --- a/apps/docs/lib/openapi-download.test.ts +++ b/apps/docs/lib/openapi-download.test.ts @@ -33,7 +33,7 @@ describe('OpenAPI download', () => { const tags = document.tags as Array<{ name: string }> expect(document.openapi).toBe('3.1.0') - expect(Object.keys(paths)).toHaveLength(152) + expect(Object.keys(paths)).toHaveLength(157) expect(tags.map((tag) => tag.name)).toEqual([ 'Workspace Sync', 'Workflows', diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index 11411b96bb9..6fb11831860 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -868,7 +868,7 @@ "get": { "operationId": "listFileVersions", "summary": "List File Versions", - "description": "List the recorded versions of a file, newest first by default. Every content write records a version; collaborative edits and repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. Retention removes old versions by plan, always keeping the newest ten, so version numbers can have gaps.\n\nOAuth scope: `api:read`.", + "description": "List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not, and collaborative edits or repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.", "x-sim-operation": "files.versions.list", "x-oauth-scope": "api:read", "tags": ["Files"], @@ -4547,8 +4547,8 @@ }, "version": { "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991, + "minimum": 1, + "maximum": 2147483647, "description": "Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.", "examples": [3] }, @@ -4557,8 +4557,9 @@ "description": "Whether this version holds the current content of the file." }, "size": { - "type": "number", + "type": "integer", "minimum": 0, + "maximum": 9007199254740991, "description": "Size in bytes of the stored content of this version." }, "contentType": { @@ -4602,8 +4603,8 @@ "anyOf": [ { "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "minimum": 1, + "maximum": 2147483647 }, { "type": "null" @@ -4750,8 +4751,8 @@ }, "version": { "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991, + "minimum": 1, + "maximum": 2147483647, "description": "Version number that was deleted." }, "deleted": { @@ -4872,8 +4873,8 @@ }, "version": { "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991, + "minimum": 1, + "maximum": 2147483647, "description": "Version the text was extracted from." } }, @@ -5316,8 +5317,8 @@ }, "currentVersion": { "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991, + "minimum": 1, + "maximum": 2147483647, "description": "Version number of the current content. List File Versions returns the history; pass this as `expectedCurrentVersion` to revert only if nothing changed since." } }, diff --git a/apps/sim/app/api/v2/files/utils.test.ts b/apps/sim/app/api/v2/files/utils.test.ts new file mode 100644 index 00000000000..026d9b4a1c8 --- /dev/null +++ b/apps/sim/app/api/v2/files/utils.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockFindUserEmailsByIds } = vi.hoisted(() => ({ mockFindUserEmailsByIds: vi.fn() })) + +vi.mock('@/lib/users/queries', () => ({ + findUserEmailsByIds: mockFindUserEmailsByIds, + getUserEmailsByIds: vi.fn(), + requireResolvedUserEmail: vi.fn(), +})) + +import type { WorkspaceFileVersionRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { toV2FileVersions } from '@/app/api/v2/files/utils' + +const version: WorkspaceFileVersionRecord = { + fileId: 'file-1', + version: 2, + key: 'workspace/ws/2-notes.md', + size: 10, + contentType: 'text/markdown', + source: 'collab', + authorUserIds: ['user-1', 'deleted-user'], + restoredFromVersion: null, + isCurrent: true, + createdAt: new Date('2026-01-02T00:00:00Z'), + updatedAt: new Date('2026-01-02T00:05:00Z'), + supersededAt: null, + secretProvenance: { status: 'exact', entries: [] }, +} + +describe('toV2FileVersions', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('keeps authors whose accounts no longer exist, with a null email', async () => { + mockFindUserEmailsByIds.mockResolvedValueOnce(new Map([['user-1', 'ada@example.com']])) + + const [serialized] = await toV2FileVersions([version]) + + expect(serialized.authors).toEqual([ + { id: 'user-1', email: 'ada@example.com' }, + { id: 'deleted-user', email: null }, + ]) + expect(serialized).not.toHaveProperty('secretProvenance') + expect(serialized).not.toHaveProperty('key') + }) +}) diff --git a/apps/sim/app/api/v2/files/utils.ts b/apps/sim/app/api/v2/files/utils.ts index 4f6222db6db..4d37212aa7c 100644 --- a/apps/sim/app/api/v2/files/utils.ts +++ b/apps/sim/app/api/v2/files/utils.ts @@ -5,7 +5,11 @@ import { buildFolderPath } from '@/lib/folders/paths' import { workspaceResourceWebUrl } from '@/lib/resources' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import type { WorkspaceFileVersionRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions' -import { getUserEmailsByIds, requireResolvedUserEmail } from '@/lib/users/queries' +import { + findUserEmailsByIds, + getUserEmailsByIds, + requireResolvedUserEmail, +} from '@/lib/users/queries' import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path' /** Shared serialization for the v2 files surface. */ @@ -85,7 +89,7 @@ function serializeV2FileVersion( export async function toV2FileVersions( records: WorkspaceFileVersionRecord[] ): Promise { - const emailByUserId = await getUserEmailsByIds(records.flatMap((record) => record.authorUserIds)) + const emailByUserId = await findUserEmailsByIds(records.flatMap((record) => record.authorUserIds)) return records.map((record) => serializeV2FileVersion(record, emailByUserId)) } diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts index 434c124d22f..6471a5be984 100644 --- a/apps/sim/background/cleanup-file-versions.ts +++ b/apps/sim/background/cleanup-file-versions.ts @@ -9,7 +9,6 @@ import type { PlanCategory } from '@/lib/billing/plan-helpers' import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' import { retentionCleanupQueue } from '@/lib/cleanup/queue' import { deleteWorkspaceStorageObjects } from '@/lib/cleanup/storage-delete' -import { isUsingCloudStorage } from '@/lib/uploads' import { FILE_VERSION_RETENTION_KEEP_LATEST, MAX_SUPERSEDED_FILE_VERSIONS, @@ -115,16 +114,13 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n /** * Deletes the stored objects first and only then the rows whose objects are gone, so a failed - * delete leaves its row — and the next run retries it — instead of orphaning the object. Without - * cloud storage the objects are left in place, as the soft-delete purge leaves them. + * delete leaves its row — and the next run retries it — instead of orphaning the object. */ async function deleteVersions(rows: Array<{ id: string; key: string }>, label: string) { - const failedKeys = isUsingCloudStorage() - ? await deleteWorkspaceStorageObjects( - rows.map((row) => row.key), - label - ) - : new Set() + const failedKeys = await deleteWorkspaceStorageObjects( + rows.map((row) => row.key), + label + ) const removable = rows.filter((row) => !failedKeys.has(row.key)) let deleted = 0 for (const batch of chunkArray(removable, DEFAULT_DELETE_CHUNK_SIZE)) { diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index b358503c956..7c0d9319926 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -231,21 +231,36 @@ const VERSION_OBJECT_FILE_CHUNK_SIZE = 100 * Deletes the stored objects of every earlier version of the workspace files about to be purged, * before their version rows cascade away with the file. A file whose version objects could not all * be deleted is withheld from this purge, so its rows survive for the next run instead of leaving - * those objects unreferenced. + * those objects unreferenced. Each chunk re-reads which files are still deleted past the cutoff + * right before deleting, so a file restored since selection keeps its history; the file-row delete + * re-checks the same condition, so it skips that file too. */ async function cleanupWorkspaceFileVersionStorage( rows: WorkspaceFileScope['multiContextRows'], + retentionDate: Date, label: string ): Promise<{ rows: WorkspaceFileScope['multiContextRows']; failed: number }> { const workspaceRows = rows.filter((row) => row.context === 'workspace') - if (workspaceRows.length === 0 || !isUsingCloudStorage()) return { rows, failed: 0 } + if (workspaceRows.length === 0) return { rows, failed: 0 } const currentKeyByFileId = new Map(workspaceRows.map((row) => [row.id, row.key])) const withheldFileIds = new Set() - for (const fileIds of chunkArray( + for (const selectedFileIds of chunkArray( [...currentKeyByFileId.keys()], VERSION_OBJECT_FILE_CHUNK_SIZE )) { + const stillExpired = await cleanupDb + .select({ id: workspaceFiles.id }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, selectedFileIds), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, retentionDate) + ) + ) + const fileIds = stillExpired.map((file) => file.id) + if (fileIds.length === 0) continue const versions = await cleanupDb .select({ fileId: workspaceFileVersion.fileId, key: workspaceFileVersion.key }) .from(workspaceFileVersion) @@ -944,7 +959,11 @@ export async function runCleanupSoftDeletes( chatCleanup = await prepareChatCleanup([...doomedChatIds], label) } - const versionCleanup = await cleanupWorkspaceFileVersionStorage(fileScope.multiContextRows, label) + const versionCleanup = await cleanupWorkspaceFileVersionStorage( + fileScope.multiContextRows, + retentionDate, + label + ) if (budgets && versionCleanup.failed) throw new Error('File version storage cleanup failed') const fileCleanup = await cleanupWorkspaceFileStorage({ ...fileScope, diff --git a/apps/sim/lib/api/contracts/v2/file-versions.ts b/apps/sim/lib/api/contracts/v2/file-versions.ts index b5719092849..88f7475687a 100644 --- a/apps/sim/lib/api/contracts/v2/file-versions.ts +++ b/apps/sim/lib/api/contracts/v2/file-versions.ts @@ -21,9 +21,9 @@ import { } from '@/lib/api/contracts/v2/shared' /** - * v2 file version history contracts. Every content write — an upload, an API or editor save, a - * Sim edit, a workflow write, a revert — records a version; collaborative edits and repeated - * workflow writes from one author fold into one version per ten-minute window. Renames and moves + * v2 file version history contracts. Every content write that changes the bytes — an upload, an + * API or editor save, a Sim edit, a workflow write, a revert — records a version; collaborative + * edits and repeated workflow writes from one author fold into one version per ten-minute window. Renames and moves * are metadata changes and never create versions, so every version reads under the file's * current name. */ @@ -48,16 +48,17 @@ export const v2FileVersionAuthorSchema = z export const v2FileVersionSchema = z .object({ fileId: z.string().describe('File this version belongs to.'), - version: z - .number() - .int() - .positive() + version: versionNumberSchema .describe( 'Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.' ) .meta({ examples: [3] }), isCurrent: z.boolean().describe('Whether this version holds the current content of the file.'), - size: z.number().nonnegative().describe('Size in bytes of the stored content of this version.'), + size: z + .number() + .int() + .nonnegative() + .describe('Size in bytes of the stored content of this version.'), contentType: z.string().describe('MIME type of the stored content of this version.'), source: v2FileVersionSourceSchema, authors: z @@ -65,10 +66,7 @@ export const v2FileVersionSchema = z .describe( 'Users who wrote this version, in order of first contribution. Empty for actorless writers such as workspace API keys. A collaborative version lists every editor in its window.' ), - restoredFromVersion: z - .number() - .int() - .positive() + restoredFromVersion: versionNumberSchema .nullable() .describe('For a `revert` version, the version whose content it restored; otherwise null.'), createdAt: z @@ -116,7 +114,7 @@ export type V2ListFileVersionsQuery = z.output import('@/app/api/v2/files/[fileId]/versions/route').then((route) => route.GET), }, listKnowledgeBases: { diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index 27a113b6495..d96365d6828 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -3,7 +3,7 @@ import { mkdtempSync } from 'node:fs' import { access, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' -import { db } from '@sim/db' +import { db, dbFor } from '@sim/db' import { organization, user, @@ -54,7 +54,7 @@ describe('workspace file version history in PostgreSQL', () => { await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } await rm(fixtureStorage.root, { recursive: true, force: true }) - await db.$client.end() + await Promise.all([db.$client.end(), dbFor('cleanup').$client.end()]) }) async function seedFile(content: string) { diff --git a/apps/sim/lib/users/queries.ts b/apps/sim/lib/users/queries.ts index 867997cb966..480f2f01c22 100644 --- a/apps/sim/lib/users/queries.ts +++ b/apps/sim/lib/users/queries.ts @@ -123,6 +123,25 @@ export async function getUserEmailsByIds(userIds: readonly string[]): Promise> { + const uniqueIds = Array.from(new Set(userIds)) + if (uniqueIds.length === 0) return new Map() + if (uniqueIds.length > MAX_USER_EMAIL_BATCH) { + throw new Error(`Cannot resolve more than ${MAX_USER_EMAIL_BATCH} user emails at once`) + } + const rows = await db + .select({ id: user.id, email: user.email }) + .from(user) + .where(inArray(user.id, uniqueIds)) + return new Map(rows.map((row) => [row.id, row.email])) +} + /** Returns one previously resolved email or throws on an incomplete projection. */ export function requireResolvedUserEmail( emailByUserId: ReadonlyMap, diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts index 3214cdf11f4..21b4bfedb9b 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts @@ -8,6 +8,11 @@ const mocks = vi.hoisted(() => ({ getWorkspaceFile: vi.fn(), getShareForResource: vi.fn(), resolvePermission: vi.fn(), + getCurrentVersion: vi.fn(), +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + getCurrentWorkspaceFileVersion: mocks.getCurrentVersion, })) vi.mock('@sim/platform-authz/workspace', () => ({ @@ -25,7 +30,10 @@ vi.mock('@/lib/public-shares/share-manager', () => ({ })) import { NoWorkspaceAccessError } from '@/lib/core/application' -import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata' +import { + readWorkspaceFileMetadata, + readWorkspaceFileMetadataWithVersion, +} from '@/lib/workspace-files/application/read-workspace-file-metadata' const canonical = { fileId: 'file-1', @@ -158,3 +166,30 @@ describe('readWorkspaceFileMetadata', () => { ).rejects.toMatchObject({ code: 'not_found' }) }) }) + +describe('readWorkspaceFileMetadataWithVersion', () => { + const principal = { kind: 'session' as const, userId: 'user-1', sessionId: 'session-1' } + + beforeEach(() => { + vi.clearAllMocks() + mocks.loadContext.mockResolvedValue(canonical) + mocks.getShareForResource.mockResolvedValue(share) + mocks.resolvePermission.mockResolvedValue('admin') + }) + + it('pairs the version number with the stored object the returned record describes', async () => { + const rewritten = { ...file, key: 'workspace/ws/data-2.csv', size: 50 } + mocks.getWorkspaceFile.mockResolvedValueOnce(file).mockResolvedValueOnce(rewritten) + mocks.getCurrentVersion + .mockResolvedValueOnce({ version: 2, key: rewritten.key }) + .mockResolvedValueOnce({ version: 2, key: rewritten.key }) + + await expect( + readWorkspaceFileMetadataWithVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, + }) + ).resolves.toEqual({ file: rewritten, share, currentVersion: 2 }) + expect(mocks.getWorkspaceFile).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 250abb3c628..271d49e69f6 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -59,17 +59,28 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ execute: executeReadWorkspaceFileMetadata, }) +/** Reads a content write can race before the file record and version head agree. */ +const CURRENT_VERSION_READ_ATTEMPTS = 3 + /** * The same read plus the current version number, for the public metadata surface. Kept separate so * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. + * + * The file row and the version head are separate reads, so a content write committing between them + * would pair the old key and size with the new number; the read repeats until both describe the + * same stored object. */ export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({ operation: fileOperations.readMetadata, resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) => resolveActiveWorkspaceFileContext(input), async execute(args): Promise { - const result = await executeReadWorkspaceFileMetadata(args) - const current = await getCurrentWorkspaceFileVersion(result.file) - return { ...result, currentVersion: current.version } + for (let attempt = 1; ; attempt++) { + const result = await executeReadWorkspaceFileMetadata(args) + const current = await getCurrentWorkspaceFileVersion(result.file) + if (current.key === result.file.key || attempt === CURRENT_VERSION_READ_ATTEMPTS) { + return { ...result, currentVersion: current.version } + } + } }, }) From 98f01a8b58a6bb0d89358bdcf37d47f54e79b0ac Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 18:17:22 -0700 Subject: [PATCH 03/10] chore(db): regenerate file version migration after staging's 0364 --- .../0365_workspace_file_versions.sql | 28 + .../db/migrations/meta/0365_snapshot.json | 27994 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + 3 files changed, 28029 insertions(+) create mode 100644 packages/db/migrations/0365_workspace_file_versions.sql create mode 100644 packages/db/migrations/meta/0365_snapshot.json diff --git a/packages/db/migrations/0365_workspace_file_versions.sql b/packages/db/migrations/0365_workspace_file_versions.sql new file mode 100644 index 00000000000..e37964a79be --- /dev/null +++ b/packages/db/migrations/0365_workspace_file_versions.sql @@ -0,0 +1,28 @@ +CREATE TYPE "public"."workspace_file_version_source" AS ENUM('upload', 'user', 'api', 'copilot', 'workflow', 'collab', 'revert', 'unknown');--> statement-breakpoint +CREATE TABLE "workspace_file_version" ( + "id" text PRIMARY KEY NOT NULL, + "file_id" text NOT NULL, + "workspace_id" text NOT NULL, + "version" integer NOT NULL, + "key" text NOT NULL, + "size_bytes" bigint NOT NULL, + "content_type" text NOT NULL, + "content_hash" text, + "content_updated_at" timestamp NOT NULL, + "superseded_at" timestamp, + "source" "workspace_file_version_source" NOT NULL, + "author_user_ids" text[] DEFAULT '{}'::text[] NOT NULL, + "restored_from_version" integer, + "secret_provenance_status" text, + "secret_provenance_entries" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "workspace_file_version_provenance_status_check" CHECK ("workspace_file_version"."secret_provenance_status" IS NULL OR "workspace_file_version"."secret_provenance_status" IN ('exact', 'unknown', 'unrecorded')) +); +--> statement-breakpoint +ALTER TABLE "workspace_file_version" ADD CONSTRAINT "workspace_file_version_file_id_workspace_files_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."workspace_files"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_file_version" ADD CONSTRAINT "workspace_file_version_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "workspace_file_version_file_version_unique" ON "workspace_file_version" USING btree ("file_id","version");--> statement-breakpoint +CREATE UNIQUE INDEX "workspace_file_version_key_unique" ON "workspace_file_version" USING btree ("key");--> statement-breakpoint +CREATE INDEX "workspace_file_version_workspace_id_idx" ON "workspace_file_version" USING btree ("workspace_id","id");--> statement-breakpoint +CREATE INDEX "workspace_file_version_workspace_superseded_idx" ON "workspace_file_version" USING btree ("workspace_id","superseded_at") WHERE "workspace_file_version"."superseded_at" IS NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0365_snapshot.json b/packages/db/migrations/meta/0365_snapshot.json new file mode 100644 index 00000000000..1b8ad3da8ca --- /dev/null +++ b/packages/db/migrations/meta/0365_snapshot.json @@ -0,0 +1,27994 @@ +{ + "id": "6c3ac487-9a6a-4984-8732-57a76bab5c42", + "prevId": "8715613a-2152-4f34-95b1-876050ac4aec", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_recovery_idx": { + "name": "doc_processing_recovery_idx", + "columns": [ + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_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_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_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_search": { + "name": "embedding_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + }, + "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_binary_hnsw_idx": { + "name": "embedding_search_binary_hnsw_idx", + "columns": [ + { + "expression": "binary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_binary_hnsw_idx": { + "name": "embedding_search_384_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_binary_hnsw_idx": { + "name": "embedding_search_768_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_binary_hnsw_idx": { + "name": "embedding_search_1024_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_binary_hnsw_idx": { + "name": "embedding_search_3072_binary_hnsw_idx", + "columns": [ + { + "expression": "binary_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bit_hamming_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "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 + }, + "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 + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_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_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_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 + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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_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.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.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_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "created_at": { + "name": "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 + }, + "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 + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "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 + } + }, + "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 77500307995..05a775bd302 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2549,6 +2549,13 @@ "when": 1789773032501, "tag": "0364_workspace_file_search_direct_gin", "breakpoints": true + }, + { + "idx": 365, + "version": "7", + "when": 1789780463212, + "tag": "0365_workspace_file_versions", + "breakpoints": true } ] } From e43846c5abbac2ae30bf7e5f32b3a683aec189a1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 18:19:08 -0700 Subject: [PATCH 04/10] improvement(files): scope version rows to the write's workspace and narrow stored provenance status --- .../workspace/workspace-file-manager.ts | 1 + .../workspace/workspace-file-versions.ts | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index f191b572c93..89bce9b4389 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -1971,6 +1971,7 @@ export async function updateWorkspaceFileContent( ? currentFile.secretProvenanceVersion : 1 const recorded = await recordWorkspaceFileVersionInTx(tx, { + workspaceId, head: versionHead, previous: currentFile, previousProvenance, diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index 2958c74a47d..ab29fbed11f 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -145,6 +145,8 @@ async function supersedeVersionInTx(tx: DbTransaction, versionId: string, now: D } interface RecordWorkspaceFileVersionParams { + /** The workspace the write was scoped to; the file row's column is nullable for other contexts. */ + workspaceId: string /** Head row loaded before the file row was updated. */ head: WorkspaceFileVersionRow | undefined /** The file row as it was before this write (locked). */ @@ -198,7 +200,7 @@ export async function recordWorkspaceFileVersionInTx( .values({ id: generateId(), fileId: previous.id, - workspaceId: next.workspaceId as string, + workspaceId: params.workspaceId, version: (head?.version ?? 0) + 1, ...contentColumns(previous, params.previousProvenance), contentHash: null, @@ -248,13 +250,13 @@ export async function recordWorkspaceFileVersionInTx( /** Inserts the new current version described by a write. */ async function insertVersion( tx: DbTransaction, - { next, nextProvenance, contentHash, write, now }: RecordWorkspaceFileVersionParams, + { workspaceId, next, nextProvenance, contentHash, write, now }: RecordWorkspaceFileVersionParams, version: number ): Promise { await tx.insert(workspaceFileVersion).values({ id: generateId(), fileId: next.id, - workspaceId: next.workspaceId as string, + workspaceId, version, ...contentColumns(next, nextProvenance), contentHash, @@ -334,6 +336,14 @@ export interface WorkspaceFileVersionRecord { secretProvenance: WorkspaceFileSecretProvenanceSnapshot } +/** Reads a stored status back, treating anything unrecognized as unknown so a revert fails closed. */ +function toSnapshotStatus(status: string | null): WorkspaceFileSecretProvenanceSnapshot['status'] { + if (status === null || status === 'exact' || status === 'unknown' || status === 'unrecorded') { + return status + } + return 'unknown' +} + function toVersionRecord(row: WorkspaceFileVersionRow): WorkspaceFileVersionRecord { return { fileId: row.fileId, @@ -349,7 +359,7 @@ function toVersionRecord(row: WorkspaceFileVersionRow): WorkspaceFileVersionReco updatedAt: row.updatedAt, supersededAt: row.supersededAt, secretProvenance: { - status: row.secretProvenanceStatus as WorkspaceFileSecretProvenanceSnapshot['status'], + status: toSnapshotStatus(row.secretProvenanceStatus), entries: row.secretProvenanceEntries, }, } From dc4f76c29cf673045fb1fec58d7f7aa30901b76d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 18:34:01 -0700 Subject: [PATCH 05/10] fix(files): release purged file history atomically with restore --- apps/docs/openapi-v2-files-audit.json | 2 +- apps/sim/background/cleanup-file-versions.ts | 17 +++-- .../background/cleanup-soft-deletes.test.ts | 40 ++++++++++ apps/sim/background/cleanup-soft-deletes.ts | 75 ++----------------- .../sim/lib/api/contracts/v2/file-versions.ts | 2 +- apps/sim/lib/cleanup/storage-delete.ts | 26 ------- .../file-versions.integration.ts | 47 ++++++++++++ .../workspace/workspace-file-versions.ts | 62 ++++++++++++++- 8 files changed, 168 insertions(+), 103 deletions(-) delete mode 100644 apps/sim/lib/cleanup/storage-delete.ts diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index 6fb11831860..cbdab44fd5c 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -4549,7 +4549,7 @@ "type": "integer", "minimum": 1, "maximum": 2147483647, - "description": "Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.", + "description": "Version number, increasing by one per recorded version. Numbers are never reused, so a gap means an older version was removed by retention or deleted.", "examples": [3] }, "isCurrent": { diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts index 6471a5be984..d982bf1286d 100644 --- a/apps/sim/background/cleanup-file-versions.ts +++ b/apps/sim/background/cleanup-file-versions.ts @@ -8,7 +8,7 @@ import type { CleanupJobPayload } from '@/lib/billing/cleanup-dispatcher' import type { PlanCategory } from '@/lib/billing/plan-helpers' import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' import { retentionCleanupQueue } from '@/lib/cleanup/queue' -import { deleteWorkspaceStorageObjects } from '@/lib/cleanup/storage-delete' +import { StorageService } from '@/lib/uploads' import { FILE_VERSION_RETENTION_KEEP_LATEST, MAX_SUPERSEDED_FILE_VERSIONS, @@ -117,10 +117,17 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n * delete leaves its row — and the next run retries it — instead of orphaning the object. */ async function deleteVersions(rows: Array<{ id: string; key: string }>, label: string) { - const failedKeys = await deleteWorkspaceStorageObjects( - rows.map((row) => row.key), - label - ) + const failedKeys = new Set() + for (const batch of chunkArray(rows, DEFAULT_DELETE_CHUNK_SIZE)) { + const deletion = await StorageService.deleteFiles( + batch.map((row) => row.key), + 'workspace' + ) + for (const { key, error } of deletion.failed) { + failedKeys.add(key) + logger.error(`[${label}] Failed to delete file version object ${key}`, { error }) + } + } const removable = rows.filter((row) => !failedKeys.has(row.key)) let deleted = 0 for (const batch of chunkArray(removable, DEFAULT_DELETE_CHUNK_SIZE)) { diff --git a/apps/sim/background/cleanup-soft-deletes.test.ts b/apps/sim/background/cleanup-soft-deletes.test.ts index cb9d262cdf4..537cbaec217 100644 --- a/apps/sim/background/cleanup-soft-deletes.test.ts +++ b/apps/sim/background/cleanup-soft-deletes.test.ts @@ -82,6 +82,13 @@ vi.mock('@/lib/uploads', () => ({ vi.mock('@/lib/uploads/server/metadata', () => ({ deleteFileMetadata: mockDeleteFileMetadata })) +const { mockReleaseExpiredWorkspaceFileVersions } = vi.hoisted(() => ({ + mockReleaseExpiredWorkspaceFileVersions: vi.fn(), +})) +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ + releaseExpiredWorkspaceFileVersions: mockReleaseExpiredWorkspaceFileVersions, +})) + vi.mock('@/lib/workflows/utils', () => ({ deduplicateWorkflowName: mockDeduplicateWorkflowName, })) @@ -122,6 +129,39 @@ describe('cleanup soft deletes', () => { }) }) + it('releases the version history of expired workspace files before purging their objects', async () => { + mockSelectRowsByIdChunks + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + id: 'file-1', + key: 'workspace/ws-1/file-1', + workspaceId: 'ws-1', + context: 'workspace', + sizeBytes: 5, + }, + { + id: 'chat-file', + key: 'mothership/chat-file', + workspaceId: 'ws-1', + context: 'mothership', + sizeBytes: 3, + }, + ]) + + await runCleanupSoftDeletes(basePayload) + + expect(mockReleaseExpiredWorkspaceFileVersions).toHaveBeenCalledWith( + expect.anything(), + ['file-1'], + expect.any(Date) + ) + expect(mockReleaseExpiredWorkspaceFileVersions.mock.invocationCallOrder[0]).toBeLessThan( + mockDeleteFiles.mock.invocationCallOrder[0] + ) + }) + it('keeps metadata rows whose object deletion failed', async () => { mockSelectRowsByIdChunks .mockResolvedValueOnce([]) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 7c0d9319926..7cf86783586 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -11,7 +11,6 @@ import { workflowMcpServer, workspaceFile, workspaceFiles, - workspaceFileVersion, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { chunkArray } from '@sim/utils/helpers' @@ -40,12 +39,12 @@ import { cleanupOwnerCondition, resolveCleanupOwnerScope, } from '@/lib/cleanup/resource-scope' -import { deleteWorkspaceStorageObjects } from '@/lib/cleanup/storage-delete' import { deduplicateFolderName } from '@/lib/folders/naming' import { hardDeleteDocuments } from '@/lib/knowledge/documents/service' import type { StorageContext } from '@/lib/uploads' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' import { allocateUniqueWorkspaceFileName } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { releaseExpiredWorkspaceFileVersions } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { deleteFileMetadata } from '@/lib/uploads/server/metadata' import { getWorkspaceFileSize } from '@/lib/uploads/shared/types' import { deduplicateWorkflowName } from '@/lib/workflows/utils' @@ -224,64 +223,6 @@ async function cleanupWorkspaceFileStorage( return result } -/** Files whose version rows are read in one query while their objects are collected. */ -const VERSION_OBJECT_FILE_CHUNK_SIZE = 100 - -/** - * Deletes the stored objects of every earlier version of the workspace files about to be purged, - * before their version rows cascade away with the file. A file whose version objects could not all - * be deleted is withheld from this purge, so its rows survive for the next run instead of leaving - * those objects unreferenced. Each chunk re-reads which files are still deleted past the cutoff - * right before deleting, so a file restored since selection keeps its history; the file-row delete - * re-checks the same condition, so it skips that file too. - */ -async function cleanupWorkspaceFileVersionStorage( - rows: WorkspaceFileScope['multiContextRows'], - retentionDate: Date, - label: string -): Promise<{ rows: WorkspaceFileScope['multiContextRows']; failed: number }> { - const workspaceRows = rows.filter((row) => row.context === 'workspace') - if (workspaceRows.length === 0) return { rows, failed: 0 } - - const currentKeyByFileId = new Map(workspaceRows.map((row) => [row.id, row.key])) - const withheldFileIds = new Set() - for (const selectedFileIds of chunkArray( - [...currentKeyByFileId.keys()], - VERSION_OBJECT_FILE_CHUNK_SIZE - )) { - const stillExpired = await cleanupDb - .select({ id: workspaceFiles.id }) - .from(workspaceFiles) - .where( - and( - inArray(workspaceFiles.id, selectedFileIds), - isNotNull(workspaceFiles.deletedAt), - lt(workspaceFiles.deletedAt, retentionDate) - ) - ) - const fileIds = stillExpired.map((file) => file.id) - if (fileIds.length === 0) continue - const versions = await cleanupDb - .select({ fileId: workspaceFileVersion.fileId, key: workspaceFileVersion.key }) - .from(workspaceFileVersion) - .where(inArray(workspaceFileVersion.fileId, fileIds)) - const fileIdByKey = new Map( - versions - .filter((version) => version.key !== currentKeyByFileId.get(version.fileId)) - .map((version) => [version.key, version.fileId]) - ) - for (const key of await deleteWorkspaceStorageObjects([...fileIdByKey.keys()], label)) { - const fileId = fileIdByKey.get(key) - if (fileId) withheldFileIds.add(fileId) - } - } - - return { - rows: rows.filter((row) => !withheldFileIds.has(row.id)), - failed: withheldFileIds.size, - } -} - async function deleteExpiredLegacyWorkspaceFileRows( rows: WorkspaceFileScope['legacyRows'], retentionDate: Date, @@ -959,16 +900,12 @@ export async function runCleanupSoftDeletes( chatCleanup = await prepareChatCleanup([...doomedChatIds], label) } - const versionCleanup = await cleanupWorkspaceFileVersionStorage( - fileScope.multiContextRows, - retentionDate, - label + await releaseExpiredWorkspaceFileVersions( + cleanupDb, + fileScope.multiContextRows.filter((row) => row.context === 'workspace').map((row) => row.id), + retentionDate ) - if (budgets && versionCleanup.failed) throw new Error('File version storage cleanup failed') - const fileCleanup = await cleanupWorkspaceFileStorage({ - ...fileScope, - multiContextRows: versionCleanup.rows, - }) + const fileCleanup = await cleanupWorkspaceFileStorage(fileScope) if (budgets && fileCleanup.filesFailed) throw new Error('File storage cleanup failed') let totalDeleted = 0 diff --git a/apps/sim/lib/api/contracts/v2/file-versions.ts b/apps/sim/lib/api/contracts/v2/file-versions.ts index 88f7475687a..8a948a83a74 100644 --- a/apps/sim/lib/api/contracts/v2/file-versions.ts +++ b/apps/sim/lib/api/contracts/v2/file-versions.ts @@ -50,7 +50,7 @@ export const v2FileVersionSchema = z fileId: z.string().describe('File this version belongs to.'), version: versionNumberSchema .describe( - 'Version number, increasing by one per recorded version. Numbers are never reused, so a gap means retention removed that version.' + 'Version number, increasing by one per recorded version. Numbers are never reused, so a gap means an older version was removed by retention or deleted.' ) .meta({ examples: [3] }), isCurrent: z.boolean().describe('Whether this version holds the current content of the file.'), diff --git a/apps/sim/lib/cleanup/storage-delete.ts b/apps/sim/lib/cleanup/storage-delete.ts deleted file mode 100644 index af7003db26f..00000000000 --- a/apps/sim/lib/cleanup/storage-delete.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createLogger } from '@sim/logger' -import { chunkArray } from '@sim/utils/helpers' -import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' -import { StorageService } from '@/lib/uploads' - -const logger = createLogger('CleanupStorageDelete') - -/** - * Deletes workspace storage objects in provider-sized chunks and returns the keys that could not be - * deleted, so callers keep the rows that reference them for the next run instead of orphaning the - * objects. Missing objects count as deleted. - */ -export async function deleteWorkspaceStorageObjects( - keys: readonly string[], - label: string -): Promise> { - const failedKeys = new Set() - for (const batch of chunkArray([...keys], DEFAULT_DELETE_CHUNK_SIZE)) { - const deletion = await StorageService.deleteFiles(batch, 'workspace') - for (const { key, error } of deletion.failed) { - failedKeys.add(key) - logger.error(`[${label}] Failed to delete storage object ${key}`, { error }) - } - } - return failedKeys -} diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index d96365d6828..ff5067b93d7 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -6,9 +6,11 @@ import path from 'node:path' import { db, dbFor } from '@sim/db' import { organization, + outboxEvent, user, workspace, workspaceFileSecretProvenance, + workspaceFiles, workspaceFileVersion, } from '@sim/db/schema' import { generateId } from '@sim/utils/id' @@ -33,9 +35,11 @@ import { updateWorkspaceFileContent, uploadWorkspaceFile, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { getCurrentWorkspaceFileVersion, queryWorkspaceFileVersions, + releaseExpiredWorkspaceFileVersions, } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { revertWorkspaceFileVersion } from '@/lib/workspace-files/application/file-versions' import { runCleanupFileVersions } from '@/background/cleanup-file-versions' @@ -398,6 +402,49 @@ describe('workspace file version history in PostgreSQL', () => { ).rejects.toMatchObject({ code: 'conflict' }) }) + it('releases an expired file history atomically and leaves a restored file untouched', async () => { + const expired = await seedFile('one') + const restored = await seedFile('one') + for (const fixture of [expired, restored]) { + for (const content of ['two', 'three']) { + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from(content), + undefined, + { version: { source: 'api', authorUserId: fixture.aliceId } } + ) + } + } + const deletedAt = sql`now() - interval '40 days'` + await db.update(workspaceFiles).set({ deletedAt }).where(eq(workspaceFiles.id, expired.fileId)) + const releasedKeys = (await versionRows(expired.fileId)) + .filter((row) => row.supersededAt !== null) + .map((row) => row.key) + + await releaseExpiredWorkspaceFileVersions( + db, + [expired.fileId, restored.fileId], + new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + ) + + expect((await versionRows(expired.fileId)).map((row) => row.version)).toEqual([3]) + expect((await versionRows(restored.fileId)).map((row) => row.version)).toEqual([1, 2, 3]) + const events = await db + .select({ id: outboxEvent.id, payload: outboxEvent.payload }) + .from(outboxEvent) + .where(eq(outboxEvent.eventType, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT)) + const enqueuedKeys = events.map((event) => (event.payload as { key: string }).key) + expect(enqueuedKeys).toEqual(expect.arrayContaining(releasedKeys)) + await db.delete(outboxEvent).where( + inArray( + outboxEvent.id, + events.map((event) => event.id) + ) + ) + }) + it('prunes superseded versions past retention but always keeps the newest ten', async () => { const fixture = await seedFile('v1') for (let index = 2; index <= 13; index++) { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index ab29fbed11f..dd8a931d37b 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -3,11 +3,12 @@ import { type WorkspaceFileRow, type WorkspaceFileVersionRow, type WorkspaceFileVersionSource, + workspaceFiles, workspaceFileVersion, } from '@sim/db/schema' import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' -import { and, desc, eq, inArray, isNotNull } from 'drizzle-orm' +import { and, desc, eq, inArray, isNotNull, lt, notInArray } from 'drizzle-orm' import { type CursorKey, type KeysetKey, @@ -21,6 +22,7 @@ import { import type { DbOrTx, DbTransaction } from '@/lib/db/types' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import type { WorkspaceFileSecretProvenanceSnapshot } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' +import { enqueueWorkspaceFileStorageCleanups } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { getWorkspaceFileSize } from '@/lib/uploads/shared/types' /** A coalescing version stops absorbing writes this long after it was opened. */ @@ -290,6 +292,64 @@ export async function deleteWorkspaceFileVersionInTx( return deleted?.key ?? null } +/** Files whose history is released in one transaction. */ +const RELEASE_FILE_CHUNK_SIZE = 100 +/** Most cleanup events one outbox insert may carry. */ +const RELEASE_ENQUEUE_CHUNK_SIZE = 1000 + +/** + * Releases the history of soft-deleted files about to be purged: per chunk, one transaction locks + * the files still deleted before `deletedBefore`, deletes their superseded version rows, and + * enqueues those objects on the durable storage-cleanup outbox. The row lock orders this against a + * restore, which updates the same row, so a file is either restored first and keeps its whole + * history or purged first and restored without it — version rows never outlive their bytes, and no + * object is left unreferenced. The current version is left to the file's own purge. + */ +export async function releaseExpiredWorkspaceFileVersions( + client: typeof db, + fileIds: readonly string[], + deletedBefore: Date +): Promise { + for (let start = 0; start < fileIds.length; start += RELEASE_FILE_CHUNK_SIZE) { + const chunk = fileIds.slice(start, start + RELEASE_FILE_CHUNK_SIZE) + await client.transaction(async (tx) => { + const expired = await tx + .select({ id: workspaceFiles.id, key: workspaceFiles.key }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, chunk), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, deletedBefore) + ) + ) + .for('update') + if (expired.length === 0) return + const released = await tx + .delete(workspaceFileVersion) + .where( + and( + inArray( + workspaceFileVersion.fileId, + expired.map((file) => file.id) + ), + notInArray( + workspaceFileVersion.key, + expired.map((file) => file.key) + ) + ) + ) + .returning({ key: workspaceFileVersion.key }) + for (let offset = 0; offset < released.length; offset += RELEASE_ENQUEUE_CHUNK_SIZE) { + await enqueueWorkspaceFileStorageCleanups( + tx, + released.slice(offset, offset + RELEASE_ENQUEUE_CHUNK_SIZE).map((row) => row.key) + ) + } + }) + } +} + /** Deletes superseded versions beyond {@link MAX_SUPERSEDED_FILE_VERSIONS}, returning their keys. */ async function pruneExcessWorkspaceFileVersionsInTx( tx: DbTransaction, From cc2cbc0109ca99e1205b930fda01de83f536aab5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 18:46:54 -0700 Subject: [PATCH 06/10] fix(files): release file history through the outbox after the current object is gone --- apps/sim/background/cleanup-file-versions.ts | 68 ++++++++----------- .../background/cleanup-soft-deletes.test.ts | 23 +++++-- apps/sim/background/cleanup-soft-deletes.ts | 8 ++- .../file-versions.integration.ts | 34 ++++++++-- 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts index d982bf1286d..2106cbde841 100644 --- a/apps/sim/background/cleanup-file-versions.ts +++ b/apps/sim/background/cleanup-file-versions.ts @@ -8,7 +8,7 @@ import type { CleanupJobPayload } from '@/lib/billing/cleanup-dispatcher' import type { PlanCategory } from '@/lib/billing/plan-helpers' import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' import { retentionCleanupQueue } from '@/lib/cleanup/queue' -import { StorageService } from '@/lib/uploads' +import { enqueueWorkspaceFileStorageCleanups } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { FILE_VERSION_RETENTION_KEEP_LATEST, MAX_SUPERSEDED_FILE_VERSIONS, @@ -85,7 +85,6 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n const ranked = cleanupDb .select({ id: workspaceFileVersion.id, - key: workspaceFileVersion.key, supersededAt: workspaceFileVersion.supersededAt, rank: sql`row_number() over (partition by ${workspaceFileVersion.fileId} order by ${workspaceFileVersion.version} desc)`.as( 'rank' @@ -101,7 +100,7 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n .as('ranked') return cleanupDb - .select({ id: ranked.id, key: ranked.key }) + .select({ id: ranked.id }) .from(ranked) .where( and( @@ -113,39 +112,34 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n } /** - * Deletes the stored objects first and only then the rows whose objects are gone, so a failed - * delete leaves its row — and the next run retries it — instead of orphaning the object. + * Deletes the expired version rows and enqueues their stored objects on the storage-cleanup outbox + * in the same transaction, so a row never outlives its release and every released object is + * deleted durably — the outbox retries failures and treats an already-missing object as done. */ -async function deleteVersions(rows: Array<{ id: string; key: string }>, label: string) { - const failedKeys = new Set() - for (const batch of chunkArray(rows, DEFAULT_DELETE_CHUNK_SIZE)) { - const deletion = await StorageService.deleteFiles( - batch.map((row) => row.key), - 'workspace' - ) - for (const { key, error } of deletion.failed) { - failedKeys.add(key) - logger.error(`[${label}] Failed to delete file version object ${key}`, { error }) - } - } - const removable = rows.filter((row) => !failedKeys.has(row.key)) +async function deleteVersions(rows: Array<{ id: string }>) { let deleted = 0 - for (const batch of chunkArray(removable, DEFAULT_DELETE_CHUNK_SIZE)) { - const removed = await cleanupDb - .delete(workspaceFileVersion) - .where( - and( - inArray( - workspaceFileVersion.id, - batch.map((row) => row.id) - ), - isNotNull(workspaceFileVersion.supersededAt) + for (const batch of chunkArray(rows, DEFAULT_DELETE_CHUNK_SIZE)) { + deleted += await cleanupDb.transaction(async (tx) => { + const removed = await tx + .delete(workspaceFileVersion) + .where( + and( + inArray( + workspaceFileVersion.id, + batch.map((row) => row.id) + ), + isNotNull(workspaceFileVersion.supersededAt) + ) ) + .returning({ key: workspaceFileVersion.key }) + await enqueueWorkspaceFileStorageCleanups( + tx, + removed.map((row) => row.key) ) - .returning({ id: workspaceFileVersion.id }) - deleted += removed.length + return removed.length + }) } - return { deleted, failed: rows.length - removable.length } + return deleted } export async function runCleanupFileVersions(payload: CleanupJobPayload): Promise { @@ -163,25 +157,21 @@ export async function runCleanupFileVersions(payload: CleanupJobPayload): Promis ) let deleted = 0 - let failed = 0 for (const group of chunkArray(workspaceIds, WORKSPACES_PER_QUERY)) { const candidates = await selectCandidateFileIds(group, cutoff, maxSuperseded) for (const fileIds of chunkArray(candidates, FILES_PER_QUERY)) { for (let batch = 0; batch < MAX_BATCHES_PER_CHUNK; batch++) { const expired = await selectExpiredVersions(fileIds, cutoff, maxSuperseded) if (expired.length === 0) break - const result = await deleteVersions(expired, label) - deleted += result.deleted - failed += result.failed - if (expired.length < VERSIONS_PER_BATCH || result.deleted === 0) break + const removed = await deleteVersions(expired) + deleted += removed + if (expired.length < VERSIONS_PER_BATCH || removed === 0) break } } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) - logger.info( - `[${label}] File version cleanup: ${deleted} deleted, ${failed} failed in ${elapsed}s` - ) + logger.info(`[${label}] File version cleanup: ${deleted} released in ${elapsed}s`) } export const cleanupFileVersionsTask = task({ diff --git a/apps/sim/background/cleanup-soft-deletes.test.ts b/apps/sim/background/cleanup-soft-deletes.test.ts index 537cbaec217..033f7741919 100644 --- a/apps/sim/background/cleanup-soft-deletes.test.ts +++ b/apps/sim/background/cleanup-soft-deletes.test.ts @@ -129,18 +129,25 @@ describe('cleanup soft deletes', () => { }) }) - it('releases the version history of expired workspace files before purging their objects', async () => { + it('releases version history only for files whose current object was deleted', async () => { mockSelectRowsByIdChunks .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { - id: 'file-1', - key: 'workspace/ws-1/file-1', + id: 'file-purged', + key: 'workspace/ws-1/file-purged', workspaceId: 'ws-1', context: 'workspace', sizeBytes: 5, }, + { + id: 'file-failed', + key: 'workspace/ws-1/file-failed', + workspaceId: 'ws-1', + context: 'workspace', + sizeBytes: 4, + }, { id: 'chat-file', key: 'mothership/chat-file', @@ -149,16 +156,20 @@ describe('cleanup soft deletes', () => { sizeBytes: 3, }, ]) + mockDeleteFiles.mockResolvedValueOnce({ + deleted: 1, + failed: [{ key: 'workspace/ws-1/file-failed', error: 'storage unavailable' }], + }) await runCleanupSoftDeletes(basePayload) expect(mockReleaseExpiredWorkspaceFileVersions).toHaveBeenCalledWith( expect.anything(), - ['file-1'], + ['file-purged'], expect.any(Date) ) - expect(mockReleaseExpiredWorkspaceFileVersions.mock.invocationCallOrder[0]).toBeLessThan( - mockDeleteFiles.mock.invocationCallOrder[0] + expect(mockDeleteFiles.mock.invocationCallOrder[0]).toBeLessThan( + mockReleaseExpiredWorkspaceFileVersions.mock.invocationCallOrder[0] ) }) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 7cf86783586..036bcf28c59 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -900,12 +900,16 @@ export async function runCleanupSoftDeletes( chatCleanup = await prepareChatCleanup([...doomedChatIds], label) } + const fileCleanup = await cleanupWorkspaceFileStorage(fileScope) + /** + * Only files whose current object is gone are committed to the purge, so a file whose object + * deletion failed keeps its history for the retry instead of losing it ahead of its own delete. + */ await releaseExpiredWorkspaceFileVersions( cleanupDb, - fileScope.multiContextRows.filter((row) => row.context === 'workspace').map((row) => row.id), + fileCleanup.multiContextRows.filter((row) => row.context === 'workspace').map((row) => row.id), retentionDate ) - const fileCleanup = await cleanupWorkspaceFileStorage(fileScope) if (budgets && fileCleanup.filesFailed) throw new Error('File storage cleanup failed') let totalDeleted = 0 diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index ff5067b93d7..0772b8bcfee 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -14,7 +14,7 @@ import { workspaceFileVersion, } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { asc, eq, inArray, sql } from 'drizzle-orm' +import { and, asc, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' const fixtureStorage = vi.hoisted(() => ({ root: '' })) @@ -434,9 +434,15 @@ describe('workspace file version history in PostgreSQL', () => { const events = await db .select({ id: outboxEvent.id, payload: outboxEvent.payload }) .from(outboxEvent) - .where(eq(outboxEvent.eventType, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT)) - const enqueuedKeys = events.map((event) => (event.payload as { key: string }).key) - expect(enqueuedKeys).toEqual(expect.arrayContaining(releasedKeys)) + .where( + and( + eq(outboxEvent.eventType, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT), + inArray(sql`${outboxEvent.payload}::jsonb ->> 'key'`, releasedKeys) + ) + ) + expect(events.map((event) => (event.payload as { key: string }).key).sort()).toEqual( + [...releasedKeys].sort() + ) await db.delete(outboxEvent).where( inArray( outboxEvent.id, @@ -464,6 +470,10 @@ describe('workspace file version history in PostgreSQL', () => { sql`${workspaceFileVersion.fileId} = ${fixture.fileId} AND ${workspaceFileVersion.supersededAt} IS NOT NULL` ) + const prunedKeys = (await versionRows(fixture.fileId)) + .filter((row) => row.version <= 3) + .map((row) => row.key) + await runCleanupFileVersions({ plan: 'pro', workspaceIds: [fixture.workspaceId], @@ -474,5 +484,21 @@ describe('workspace file version history in PostgreSQL', () => { const rows = await versionRows(fixture.fileId) expect(rows.map((row) => row.version)).toEqual([4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) expect(rows.at(-1)?.supersededAt).toBeNull() + const events = await db + .select({ id: outboxEvent.id }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.eventType, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT), + inArray(sql`${outboxEvent.payload}::jsonb ->> 'key'`, prunedKeys) + ) + ) + expect(events).toHaveLength(prunedKeys.length) + await db.delete(outboxEvent).where( + inArray( + outboxEvent.id, + events.map((event) => event.id) + ) + ) }) }) From 2c65f4bd1733e8452d6ea7a9601199a3e4da13f3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 19:04:39 -0700 Subject: [PATCH 07/10] fix(files): release purged history inside the file-row purge transaction --- apps/docs/openapi-v2-files-audit.json | 2 +- apps/sim/background/cleanup-file-versions.ts | 8 +- .../background/cleanup-soft-deletes.test.ts | 28 ++-- apps/sim/background/cleanup-soft-deletes.ts | 16 +-- .../sim/lib/api/contracts/v2/file-versions.ts | 10 +- .../file-versions.integration.ts | 19 ++- .../workspace-file-storage-accounting.test.ts | 7 +- .../workspace/workspace-file-versions.ts | 124 +++++++++--------- .../read-workspace-file-metadata.ts | 2 +- 9 files changed, 108 insertions(+), 108 deletions(-) diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index cbdab44fd5c..ab1cad29ad5 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -4569,7 +4569,7 @@ "source": { "type": "string", "enum": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"], - "description": "What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed)." + "description": "What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed, or by a writer with no source of its own)." }, "authors": { "type": "array", diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts index 2106cbde841..2f4b9a89ad2 100644 --- a/apps/sim/background/cleanup-file-versions.ts +++ b/apps/sim/background/cleanup-file-versions.ts @@ -9,10 +9,7 @@ import type { PlanCategory } from '@/lib/billing/plan-helpers' import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' import { retentionCleanupQueue } from '@/lib/cleanup/queue' import { enqueueWorkspaceFileStorageCleanups } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' -import { - FILE_VERSION_RETENTION_KEEP_LATEST, - MAX_SUPERSEDED_FILE_VERSIONS, -} from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { MAX_SUPERSEDED_FILE_VERSIONS } from '@/lib/uploads/contexts/workspace/workspace-file-versions' const logger = createLogger('CleanupFileVersions') @@ -39,6 +36,9 @@ const MAX_SUPERSEDED_VERSIONS_BY_PLAN: Record = { enterprise: MAX_SUPERSEDED_FILE_VERSIONS, } +/** Retention never prunes the newest versions of a file, whatever their age. */ +const FILE_VERSION_RETENTION_KEEP_LATEST = 10 + /** Newest superseded versions a file keeps whatever their age (the current version is the tenth). */ const KEEP_SUPERSEDED = FILE_VERSION_RETENTION_KEEP_LATEST - 1 diff --git a/apps/sim/background/cleanup-soft-deletes.test.ts b/apps/sim/background/cleanup-soft-deletes.test.ts index 033f7741919..625a920ff47 100644 --- a/apps/sim/background/cleanup-soft-deletes.test.ts +++ b/apps/sim/background/cleanup-soft-deletes.test.ts @@ -82,11 +82,11 @@ vi.mock('@/lib/uploads', () => ({ vi.mock('@/lib/uploads/server/metadata', () => ({ deleteFileMetadata: mockDeleteFileMetadata })) -const { mockReleaseExpiredWorkspaceFileVersions } = vi.hoisted(() => ({ - mockReleaseExpiredWorkspaceFileVersions: vi.fn(), +const { mockReleaseWorkspaceFileVersionsForPurgeInTx } = vi.hoisted(() => ({ + mockReleaseWorkspaceFileVersionsForPurgeInTx: vi.fn(), })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ - releaseExpiredWorkspaceFileVersions: mockReleaseExpiredWorkspaceFileVersions, + releaseWorkspaceFileVersionsForPurgeInTx: mockReleaseWorkspaceFileVersionsForPurgeInTx, })) vi.mock('@/lib/workflows/utils', () => ({ @@ -129,7 +129,7 @@ describe('cleanup soft deletes', () => { }) }) - it('releases version history only for files whose current object was deleted', async () => { + it('releases version history inside the transaction that purges the file rows', async () => { mockSelectRowsByIdChunks .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) @@ -148,28 +148,26 @@ describe('cleanup soft deletes', () => { context: 'workspace', sizeBytes: 4, }, - { - id: 'chat-file', - key: 'mothership/chat-file', - workspaceId: 'ws-1', - context: 'mothership', - sizeBytes: 3, - }, ]) mockDeleteFiles.mockResolvedValueOnce({ deleted: 1, failed: [{ key: 'workspace/ws-1/file-failed', error: 'storage unavailable' }], }) + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'file-purged', sizeBytes: 5 }]) await runCleanupSoftDeletes(basePayload) - expect(mockReleaseExpiredWorkspaceFileVersions).toHaveBeenCalledWith( - expect.anything(), + expect(mockReleaseWorkspaceFileVersionsForPurgeInTx).toHaveBeenCalledOnce() + expect(mockReleaseWorkspaceFileVersionsForPurgeInTx).toHaveBeenCalledWith( + dbChainMock.db, ['file-purged'], expect.any(Date) ) - expect(mockDeleteFiles.mock.invocationCallOrder[0]).toBeLessThan( - mockReleaseExpiredWorkspaceFileVersions.mock.invocationCallOrder[0] + expect(dbChainMockFns.transaction.mock.invocationCallOrder[0]).toBeLessThan( + mockReleaseWorkspaceFileVersionsForPurgeInTx.mock.invocationCallOrder[0] + ) + expect(mockReleaseWorkspaceFileVersionsForPurgeInTx.mock.invocationCallOrder[0]).toBeLessThan( + dbChainMockFns.delete.mock.invocationCallOrder[0] ) }) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 036bcf28c59..fa9ebe03925 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -44,7 +44,7 @@ import { hardDeleteDocuments } from '@/lib/knowledge/documents/service' import type { StorageContext } from '@/lib/uploads' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' import { allocateUniqueWorkspaceFileName } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { releaseExpiredWorkspaceFileVersions } from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { releaseWorkspaceFileVersionsForPurgeInTx } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { deleteFileMetadata } from '@/lib/uploads/server/metadata' import { getWorkspaceFileSize } from '@/lib/uploads/shared/types' import { deduplicateWorkflowName } from '@/lib/workflows/utils' @@ -333,6 +333,11 @@ async function deleteExpiredBillableWorkspaceFileRows( for (const batch of chunkArray(workspaceRows, DEFAULT_DELETE_CHUNK_SIZE)) { try { const deletedCount = await db.transaction(async (tx) => { + await releaseWorkspaceFileVersionsForPurgeInTx( + tx, + batch.map(({ id }) => id), + retentionDate + ) const deletedRows = await tx .delete(workspaceFiles) .where( @@ -901,15 +906,6 @@ export async function runCleanupSoftDeletes( } const fileCleanup = await cleanupWorkspaceFileStorage(fileScope) - /** - * Only files whose current object is gone are committed to the purge, so a file whose object - * deletion failed keeps its history for the retry instead of losing it ahead of its own delete. - */ - await releaseExpiredWorkspaceFileVersions( - cleanupDb, - fileCleanup.multiContextRows.filter((row) => row.context === 'workspace').map((row) => row.id), - retentionDate - ) if (budgets && fileCleanup.filesFailed) throw new Error('File storage cleanup failed') let totalDeleted = 0 diff --git a/apps/sim/lib/api/contracts/v2/file-versions.ts b/apps/sim/lib/api/contracts/v2/file-versions.ts index 8a948a83a74..4926725818c 100644 --- a/apps/sim/lib/api/contracts/v2/file-versions.ts +++ b/apps/sim/lib/api/contracts/v2/file-versions.ts @@ -22,16 +22,16 @@ import { /** * v2 file version history contracts. Every content write that changes the bytes — an upload, an - * API or editor save, a Sim edit, a workflow write, a revert — records a version; collaborative - * edits and repeated workflow writes from one author fold into one version per ten-minute window. Renames and moves - * are metadata changes and never create versions, so every version reads under the file's - * current name. + * API or editor save, a Sim edit, a workflow write, a revert — records a version. Collaborative + * edits and repeated workflow writes from one author fold into the current version until it is ten + * minutes old or its writer has been idle for five. Renames and moves are metadata changes and never + * create versions, so every version reads under the file's current name. */ export const v2FileVersionSourceSchema = z .enum(['upload', 'user', 'api', 'copilot', 'workflow', 'collab', 'revert', 'unknown']) .describe( - 'What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed).' + 'What wrote this version: `upload` (the original upload), `user` (a save in the Sim editor), `api` (an API, CLI, or MCP write), `copilot` (Sim, the agent), `workflow` (a workflow run), `collab` (collaborative editing), `revert` (a revert to an earlier version), or `unknown` (content written before version history existed, or by a writer with no source of its own).' ) export const v2FileVersionAuthorSchema = z diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index 0772b8bcfee..53226cf25c4 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -39,7 +39,7 @@ import { WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT } from '@/lib/uploads/conte import { getCurrentWorkspaceFileVersion, queryWorkspaceFileVersions, - releaseExpiredWorkspaceFileVersions, + releaseWorkspaceFileVersionsForPurgeInTx, } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { revertWorkspaceFileVersion } from '@/lib/workspace-files/application/file-versions' import { runCleanupFileVersions } from '@/background/cleanup-file-versions' @@ -402,7 +402,7 @@ describe('workspace file version history in PostgreSQL', () => { ).rejects.toMatchObject({ code: 'conflict' }) }) - it('releases an expired file history atomically and leaves a restored file untouched', async () => { + it('releases history only with the purge transaction and leaves a restored file untouched', async () => { const expired = await seedFile('one') const restored = await seedFile('one') for (const fixture of [expired, restored]) { @@ -423,10 +423,17 @@ describe('workspace file version history in PostgreSQL', () => { .filter((row) => row.supersededAt !== null) .map((row) => row.key) - await releaseExpiredWorkspaceFileVersions( - db, - [expired.fileId, restored.fileId], - new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + await expect( + db.transaction(async (tx) => { + await releaseWorkspaceFileVersionsForPurgeInTx(tx, [expired.fileId], cutoff) + throw new Error('purge failed') + }) + ).rejects.toThrow('purge failed') + expect((await versionRows(expired.fileId)).map((row) => row.version)).toEqual([1, 2, 3]) + + await db.transaction((tx) => + releaseWorkspaceFileVersionsForPurgeInTx(tx, [expired.fileId, restored.fileId], cutoff) ) expect((await versionRows(expired.fileId)).map((row) => row.version)).toEqual([3]) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts index 38198588f4d..a03eea9ef9e 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts @@ -210,7 +210,7 @@ describe('workspace file metadata and storage accounting', () => { mockProcessWorkspaceFileLiveDocReconciliationNow.mockResolvedValue('completed') mockReplaceWorkspaceFileSecretProvenanceInTx.mockResolvedValue(undefined) mockSaveCollabDocStateInTx.mockResolvedValue(undefined) - mockRecordWorkspaceFileVersionInTx.mockResolvedValue({ releasedKeys: [] }) + mockRecordWorkspaceFileVersionInTx.mockResolvedValue({ version: 2, releasedKeys: [] }) }) it('returns the canonical inserted record with the pre-resolved folder path', async () => { @@ -785,7 +785,10 @@ describe('workspace file metadata and storage accounting', () => { dbChainMockFns.limit.mockResolvedValueOnce([FILE_ROW]).mockResolvedValueOnce([FILE_ROW]) dbChainMockFns.returning.mockResolvedValueOnce([updatedFile]) mockUploadFile.mockResolvedValueOnce({ key: replacementKey }) - mockRecordWorkspaceFileVersionInTx.mockResolvedValueOnce({ releasedKeys: [FILE_ROW.key] }) + mockRecordWorkspaceFileVersionInTx.mockResolvedValueOnce({ + version: 2, + releasedKeys: [FILE_ROW.key], + }) await updateWorkspaceFileContent( FILE_ROW.workspaceId, diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index dd8a931d37b..bc89b84fc08 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -34,8 +34,6 @@ const FILE_VERSION_COALESCE_IDLE_MS = 5 * 60 * 1000 * write loop stays bounded between retention runs. Plan-specific counts are enforced by retention. */ export const MAX_SUPERSEDED_FILE_VERSIONS = 500 -/** Retention never prunes the newest versions of a file, whatever their age. */ -export const FILE_VERSION_RETENTION_KEEP_LATEST = 10 /** * Bounds the attribution list a long collaborative window can accumulate. Also keeps a full page of * versions within the user-email batch the v2 presenter resolves in one query. @@ -98,7 +96,7 @@ export function isVersionHeadCurrent( } /** Whether a file's current bytes are still the ones originally uploaded. */ -export function isOriginalUploadContent( +function isOriginalUploadContent( file: Pick ): boolean { return ( @@ -164,7 +162,7 @@ interface RecordWorkspaceFileVersionParams { } /** The outcome of recording one content write. */ -export interface RecordedWorkspaceFileVersion { +interface RecordedWorkspaceFileVersion { /** Version number of the file's current content after the write. */ version: number /** Storage keys no version references any more; the caller releases them after commit. */ @@ -292,61 +290,55 @@ export async function deleteWorkspaceFileVersionInTx( return deleted?.key ?? null } -/** Files whose history is released in one transaction. */ -const RELEASE_FILE_CHUNK_SIZE = 100 /** Most cleanup events one outbox insert may carry. */ const RELEASE_ENQUEUE_CHUNK_SIZE = 1000 /** - * Releases the history of soft-deleted files about to be purged: per chunk, one transaction locks - * the files still deleted before `deletedBefore`, deletes their superseded version rows, and - * enqueues those objects on the durable storage-cleanup outbox. The row lock orders this against a - * restore, which updates the same row, so a file is either restored first and keeps its whole - * history or purged first and restored without it — version rows never outlive their bytes, and no - * object is left unreferenced. The current version is left to the file's own purge. + * Releases the history of soft-deleted files inside the transaction that purges their rows: it + * locks the files still deleted before `deletedBefore`, deletes their superseded version rows, and + * enqueues those objects on the durable storage-cleanup outbox. Sharing the purge's transaction + * means history is released exactly when the file row is deleted — a failed purge rolls both back — + * and the row lock orders it against a restore, which updates the same row. The current version's + * row cascades with the file, whose own object the purge deletes. */ -export async function releaseExpiredWorkspaceFileVersions( - client: typeof db, +export async function releaseWorkspaceFileVersionsForPurgeInTx( + tx: DbTransaction, fileIds: readonly string[], deletedBefore: Date ): Promise { - for (let start = 0; start < fileIds.length; start += RELEASE_FILE_CHUNK_SIZE) { - const chunk = fileIds.slice(start, start + RELEASE_FILE_CHUNK_SIZE) - await client.transaction(async (tx) => { - const expired = await tx - .select({ id: workspaceFiles.id, key: workspaceFiles.key }) - .from(workspaceFiles) - .where( - and( - inArray(workspaceFiles.id, chunk), - isNotNull(workspaceFiles.deletedAt), - lt(workspaceFiles.deletedAt, deletedBefore) - ) - ) - .for('update') - if (expired.length === 0) return - const released = await tx - .delete(workspaceFileVersion) - .where( - and( - inArray( - workspaceFileVersion.fileId, - expired.map((file) => file.id) - ), - notInArray( - workspaceFileVersion.key, - expired.map((file) => file.key) - ) - ) - ) - .returning({ key: workspaceFileVersion.key }) - for (let offset = 0; offset < released.length; offset += RELEASE_ENQUEUE_CHUNK_SIZE) { - await enqueueWorkspaceFileStorageCleanups( - tx, - released.slice(offset, offset + RELEASE_ENQUEUE_CHUNK_SIZE).map((row) => row.key) + if (fileIds.length === 0) return + const expired = await tx + .select({ id: workspaceFiles.id, key: workspaceFiles.key }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, [...fileIds]), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, deletedBefore) + ) + ) + .for('update') + if (expired.length === 0) return + const released = await tx + .delete(workspaceFileVersion) + .where( + and( + inArray( + workspaceFileVersion.fileId, + expired.map((file) => file.id) + ), + notInArray( + workspaceFileVersion.key, + expired.map((file) => file.key) ) - } - }) + ) + ) + .returning({ key: workspaceFileVersion.key }) + for (let offset = 0; offset < released.length; offset += RELEASE_ENQUEUE_CHUNK_SIZE) { + await enqueueWorkspaceFileStorageCleanups( + tx, + released.slice(offset, offset + RELEASE_ENQUEUE_CHUNK_SIZE).map((row) => row.key) + ) } } @@ -356,7 +348,7 @@ async function pruneExcessWorkspaceFileVersionsInTx( fileId: string ): Promise { const excess = await tx - .select({ id: workspaceFileVersion.id, key: workspaceFileVersion.key }) + .select({ id: workspaceFileVersion.id }) .from(workspaceFileVersion) .where( and(eq(workspaceFileVersion.fileId, fileId), isNotNull(workspaceFileVersion.supersededAt)) @@ -364,17 +356,20 @@ async function pruneExcessWorkspaceFileVersionsInTx( .orderBy(desc(workspaceFileVersion.version)) .offset(MAX_SUPERSEDED_FILE_VERSIONS) if (excess.length === 0) return [] - await tx.delete(workspaceFileVersion).where( - inArray( - workspaceFileVersion.id, - excess.map((row) => row.id) + const removed = await tx + .delete(workspaceFileVersion) + .where( + inArray( + workspaceFileVersion.id, + excess.map((row) => row.id) + ) ) - ) - return excess.map((row) => row.key) + .returning({ key: workspaceFileVersion.key }) + return removed.map((row) => row.key) } /** The file fields that identify its current bytes. */ -export type WorkspaceFileVersionSubject = Pick< +type WorkspaceFileVersionSubject = Pick< WorkspaceFileRecord, 'id' | 'key' | 'size' | 'type' | 'uploadedBy' | 'uploadedAt' | 'updatedAt' | 'contentUpdatedAt' > @@ -469,10 +464,10 @@ export async function queryWorkspaceFileVersions( .limit(options.limit + 1) const records = rows.map(toVersionRecord) /** - * A file with no rows lists its implicit version 1, the only version it has. Any cursor was minted - * past that version already, so only the first page can carry it. + * An uncursored page that is empty means the file has no rows, so it lists its implicit version 1. + * Any cursor was minted past that version already, so only the first page can carry it. */ - if (records.length === 0 && !options.after && !(await loadWorkspaceFileVersionHead(file.id))) { + if (records.length === 0 && !options.after) { records.push(implicitFirstVersion(file)) } const page = keysetPage(VERSION_KEYSET, records, options.limit) @@ -480,9 +475,10 @@ export async function queryWorkspaceFileVersions( } /** - * The subset of `keys` that name a retained version object. Such an object is reachable only - * through the version routes, which authorize against its file; key-addressed readers must refuse - * it rather than fall back to the object's own metadata or treat it as an untracked legacy file. + * The subset of `keys` held by any version row. Callers ask only about keys with no active file row, + * so a match is a superseded version or an archived file's current bytes — both reachable only + * through authorized file and version routes. Key-addressed readers refuse them rather than fall + * back to the object's own metadata or treat them as untracked legacy files. */ export async function findWorkspaceFileVersionKeys(keys: readonly string[]): Promise> { if (keys.length === 0) return new Set() diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 271d49e69f6..97c1dbe874a 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -59,7 +59,7 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ execute: executeReadWorkspaceFileMetadata, }) -/** Reads a content write can race before the file record and version head agree. */ +/** Attempts to read a file record and version head that describe the same stored object. */ const CURRENT_VERSION_READ_ATTEMPTS = 3 /** From 18188ced15e7b0c0fab78c9ca8165d0dcbf3530b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 22:13:55 -0700 Subject: [PATCH 08/10] fix(files): resolve metadata version from the record's own storage key --- .../file-versions.integration.ts | 3 +++ .../workspace/workspace-file-versions.ts | 18 +++++++++++++++ .../read-workspace-file-metadata.test.ts | 17 ++++++-------- .../read-workspace-file-metadata.ts | 22 +++++-------------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index 53226cf25c4..f9d3372b9f6 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -38,6 +38,7 @@ import { import { WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { getCurrentWorkspaceFileVersion, + getWorkspaceFileVersionNumberForRecord, queryWorkspaceFileVersions, releaseWorkspaceFileVersionsForPurgeInTx, } from '@/lib/uploads/contexts/workspace/workspace-file-versions' @@ -227,6 +228,8 @@ describe('workspace file version history in PostgreSQL', () => { const after = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) if (!after) throw new Error('file missing') expect((await getCurrentWorkspaceFileVersion(after)).version).toBe(2) + expect(await getWorkspaceFileVersionNumberForRecord(before)).toBe(1) + expect(await getWorkspaceFileVersionNumberForRecord(after)).toBe(2) }) it('does not keep an empty shell as a version of its own', async () => { diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index bc89b84fc08..994993dbcc3 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -497,6 +497,24 @@ export async function getCurrentWorkspaceFileVersion( return head ? toVersionRecord(head) : implicitFirstVersion(file) } +/** + * The version number of the content a file record describes. Matched by the record's storage key, so + * it stays exact even if a write committed after the record was read: that record's key still names + * its own version row. A key with no row is either a file with no history (implicit version 1) or + * bytes a collaborative write replaced within the same version, which keeps the head's number. + */ +export async function getWorkspaceFileVersionNumberForRecord( + file: Pick +): Promise { + const [row] = await db + .select({ version: workspaceFileVersion.version }) + .from(workspaceFileVersion) + .where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.key, file.key))) + .limit(1) + if (row) return row.version + return (await loadWorkspaceFileVersionHead(file.id))?.version ?? 1 +} + /** One version of a file, or null when it never existed or retention removed it. */ export async function getWorkspaceFileVersion( file: WorkspaceFileVersionSubject, diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts index 21b4bfedb9b..d31dde4aa8f 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts @@ -8,11 +8,11 @@ const mocks = vi.hoisted(() => ({ getWorkspaceFile: vi.fn(), getShareForResource: vi.fn(), resolvePermission: vi.fn(), - getCurrentVersion: vi.fn(), + getVersionNumberForRecord: vi.fn(), })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ - getCurrentWorkspaceFileVersion: mocks.getCurrentVersion, + getWorkspaceFileVersionNumberForRecord: mocks.getVersionNumberForRecord, })) vi.mock('@sim/platform-authz/workspace', () => ({ @@ -177,19 +177,16 @@ describe('readWorkspaceFileMetadataWithVersion', () => { mocks.resolvePermission.mockResolvedValue('admin') }) - it('pairs the version number with the stored object the returned record describes', async () => { - const rewritten = { ...file, key: 'workspace/ws/data-2.csv', size: 50 } - mocks.getWorkspaceFile.mockResolvedValueOnce(file).mockResolvedValueOnce(rewritten) - mocks.getCurrentVersion - .mockResolvedValueOnce({ version: 2, key: rewritten.key }) - .mockResolvedValueOnce({ version: 2, key: rewritten.key }) + it('reports the version of the stored object the returned record describes', async () => { + mocks.getWorkspaceFile.mockResolvedValueOnce(file) + mocks.getVersionNumberForRecord.mockResolvedValueOnce(4) await expect( readWorkspaceFileMetadataWithVersion.execute({ principal, input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, }) - ).resolves.toEqual({ file: rewritten, share, currentVersion: 2 }) - expect(mocks.getWorkspaceFile).toHaveBeenCalledTimes(2) + ).resolves.toEqual({ file, share, currentVersion: 4 }) + expect(mocks.getVersionNumberForRecord).toHaveBeenCalledWith(file) }) }) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 97c1dbe874a..0c99dc9e94e 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -7,7 +7,7 @@ import { getWorkspaceFile, type WorkspaceFileRecord, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { getCurrentWorkspaceFileVersion } from '@/lib/uploads/contexts/workspace/workspace-file-versions' +import { getWorkspaceFileVersionNumberForRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' import { fileOperations } from '@/lib/workspace-files/application/operations' import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' @@ -59,28 +59,18 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ execute: executeReadWorkspaceFileMetadata, }) -/** Attempts to read a file record and version head that describe the same stored object. */ -const CURRENT_VERSION_READ_ATTEMPTS = 3 - /** * The same read plus the current version number, for the public metadata surface. Kept separate so - * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. - * - * The file row and the version head are separate reads, so a content write committing between them - * would pair the old key and size with the new number; the read repeats until both describe the - * same stored object. + * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. The number is + * resolved from the returned record's own storage key, so it always identifies the content that + * record describes. */ export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({ operation: fileOperations.readMetadata, resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) => resolveActiveWorkspaceFileContext(input), async execute(args): Promise { - for (let attempt = 1; ; attempt++) { - const result = await executeReadWorkspaceFileMetadata(args) - const current = await getCurrentWorkspaceFileVersion(result.file) - if (current.key === result.file.key || attempt === CURRENT_VERSION_READ_ATTEMPTS) { - return { ...result, currentVersion: current.version } - } - } + const result = await executeReadWorkspaceFileMetadata(args) + return { ...result, currentVersion: await getWorkspaceFileVersionNumberForRecord(result.file) } }, }) From 5873affbf567f92471a2cb939d03449291c2a9a9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 22:28:21 -0700 Subject: [PATCH 09/10] fix(files): never pair a stale file record with a newer version number --- apps/docs/openapi-v2-files-audit.json | 5 +++- .../api/contracts/v2/openapi/files-audit.ts | 4 +-- .../lib/api/mcp/generated/v2-operations.ts | 2 +- .../file-versions.integration.ts | 28 +++++++++++++++++++ .../workspace/workspace-file-versions.ts | 12 ++++---- .../read-workspace-file-metadata.test.ts | 26 +++++++++++++++++ .../read-workspace-file-metadata.ts | 17 +++++++++-- 7 files changed, 81 insertions(+), 13 deletions(-) diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index ab1cad29ad5..27fc6ad93c7 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -2068,7 +2068,7 @@ "get": { "operationId": "getFile", "summary": "Get File Metadata", - "description": "Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.", + "description": "Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.\n\nOAuth scope: `api:read`.", "x-sim-operation": "files.read_metadata", "x-oauth-scope": "api:read", "tags": ["Files"], @@ -2145,6 +2145,9 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/Conflict" + }, "429": { "$ref": "#/components/responses/RateLimited" }, diff --git a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts index 22794ddad1c..cc9b36665ce 100644 --- a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts +++ b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts @@ -848,8 +848,8 @@ const declaredRoutes = [ operationId: 'getFile', summary: 'Get File Metadata', description: - 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.', - errors: RESOURCE_ERRORS, + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.', + errors: RESOURCE_CONFLICT_ERRORS, success: { description: 'File metadata and public-share state.' }, }), { diff --git a/apps/sim/lib/api/mcp/generated/v2-operations.ts b/apps/sim/lib/api/mcp/generated/v2-operations.ts index 89617ef136d..cf0060b69be 100644 --- a/apps/sim/lib/api/mcp/generated/v2-operations.ts +++ b/apps/sim/lib/api/mcp/generated/v2-operations.ts @@ -1035,7 +1035,7 @@ export const V2_MCP_OPERATIONS = { contract: v2GetFileContract, summary: 'Get File Metadata', description: - 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.', + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/[fileId]/metadata/route').then((route) => route.GET), }, getFileShare: { diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index f9d3372b9f6..eef4379ec78 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -271,6 +271,34 @@ describe('workspace file version history in PostgreSQL', () => { expect(await objectExists(fixture.firstKey)).toBe(false) }) + it('reports a stale record as unresolvable once a coalesced write released its key', async () => { + const fixture = await seedFile('original') + const write = { source: 'collab', authorUserId: fixture.aliceId } as const + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('draft one'), + undefined, + { version: write } + ) + const stale = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + if (!stale) throw new Error('file missing') + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('draft two'), + undefined, + { version: write } + ) + const fresh = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) + if (!fresh) throw new Error('file missing') + + expect(await getWorkspaceFileVersionNumberForRecord(stale)).toBeNull() + expect(await getWorkspaceFileVersionNumberForRecord(fresh)).toBe(2) + }) + it('never folds deliberate writes, and repoints the head for identical bytes', async () => { const fixture = await seedFile('original') const write = { source: 'api', authorUserId: fixture.aliceId } as const diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index 994993dbcc3..814d8574c1b 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -498,21 +498,21 @@ export async function getCurrentWorkspaceFileVersion( } /** - * The version number of the content a file record describes. Matched by the record's storage key, so - * it stays exact even if a write committed after the record was read: that record's key still names - * its own version row. A key with no row is either a file with no history (implicit version 1) or - * bytes a collaborative write replaced within the same version, which keeps the head's number. + * The version number of the content a file record describes, matched by the record's storage key so + * it stays exact even if a write committed after the record was read. Returns null when the record + * is stale in a way its key cannot answer — a later write replaced those bytes within their version + * and released the key — so the caller re-reads the record rather than guess. */ export async function getWorkspaceFileVersionNumberForRecord( file: Pick -): Promise { +): Promise { const [row] = await db .select({ version: workspaceFileVersion.version }) .from(workspaceFileVersion) .where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.key, file.key))) .limit(1) if (row) return row.version - return (await loadWorkspaceFileVersionHead(file.id))?.version ?? 1 + return (await loadWorkspaceFileVersionHead(file.id)) ? null : 1 } /** One version of a file, or null when it never existed or retention removed it. */ diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts index d31dde4aa8f..944420a4afa 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts @@ -189,4 +189,30 @@ describe('readWorkspaceFileMetadataWithVersion', () => { ).resolves.toEqual({ file, share, currentVersion: 4 }) expect(mocks.getVersionNumberForRecord).toHaveBeenCalledWith(file) }) + + it('re-reads a record whose bytes a concurrent write already replaced', async () => { + const rewritten = { ...file, key: 'workspace/ws/data-2.csv' } + mocks.getWorkspaceFile.mockResolvedValueOnce(file).mockResolvedValueOnce(rewritten) + mocks.getVersionNumberForRecord.mockResolvedValueOnce(null).mockResolvedValueOnce(5) + + await expect( + readWorkspaceFileMetadataWithVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, + }) + ).resolves.toEqual({ file: rewritten, share, currentVersion: 5 }) + }) + + it('answers a retryable conflict when every read is already stale', async () => { + mocks.getWorkspaceFile.mockResolvedValue(file) + mocks.getVersionNumberForRecord.mockResolvedValue(null) + + await expect( + readWorkspaceFileMetadataWithVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + expect(mocks.getVersionNumberForRecord).toHaveBeenCalledTimes(3) + }) }) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 0c99dc9e94e..71026e77514 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -59,18 +59,29 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ execute: executeReadWorkspaceFileMetadata, }) +/** Reads of a record a concurrent write keeps replacing before the version is reported as a conflict. */ +const CURRENT_VERSION_READ_ATTEMPTS = 3 + /** * The same read plus the current version number, for the public metadata surface. Kept separate so * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. The number is * resolved from the returned record's own storage key, so it always identifies the content that - * record describes. + * record describes; a record whose bytes a concurrent write already replaced is read again, and a + * file rewritten on every attempt answers a retryable conflict rather than a mismatched version. */ export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({ operation: fileOperations.readMetadata, resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) => resolveActiveWorkspaceFileContext(input), async execute(args): Promise { - const result = await executeReadWorkspaceFileMetadata(args) - return { ...result, currentVersion: await getWorkspaceFileVersionNumberForRecord(result.file) } + for (let attempt = 0; attempt < CURRENT_VERSION_READ_ATTEMPTS; attempt++) { + const result = await executeReadWorkspaceFileMetadata(args) + const currentVersion = await getWorkspaceFileVersionNumberForRecord(result.file) + if (currentVersion !== null) return { ...result, currentVersion } + } + throw new OrchestrationError( + 'conflict', + 'The file changed while it was being read; retry the request' + ) }, }) From 29a598eb2af523129d6dde69b4705c497692160d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 23:12:15 -0700 Subject: [PATCH 10/10] improvement(files): simplify file version history internals - read metadata and its current version in one statement; drop the retry and 409 - move provenance policy branching into the provenance module - project stored provenance out of list, head, and get reads; revert reads it on demand - chunk storage-cleanup enqueues to the outbox bulk limit in one place - drop the write-only content_updated_at version column (unreleased 0365) - reuse findCause, the shared cleanup batch constants, and the version-number primitives - share the v2 text presenter between the file and version routes --- apps/docs/openapi-v2-files-audit.json | 7 +- .../v2/files/[fileId]/metadata/route.test.ts | 11 +- .../api/v2/files/[fileId]/metadata/route.ts | 4 +- .../app/api/v2/files/[fileId]/text/route.ts | 16 +- .../[fileId]/versions/[version]/text/route.ts | 26 +--- apps/sim/app/api/v2/files/utils.test.ts | 1 - apps/sim/app/api/v2/files/utils.ts | 27 +++- apps/sim/background/cleanup-file-versions.ts | 87 +++++------ .../sim/lib/api/contracts/deployments.test.ts | 12 +- apps/sim/lib/api/contracts/deployments.ts | 17 +-- apps/sim/lib/api/contracts/primitives.ts | 6 +- apps/sim/lib/api/contracts/v1/workflows.ts | 9 +- .../sim/lib/api/contracts/v2/file-versions.ts | 6 +- apps/sim/lib/api/contracts/v2/logs.ts | 2 +- .../api/contracts/v2/openapi/files-audit.ts | 6 +- .../v2/workflow-deployment-requests.test.ts | 10 +- apps/sim/lib/api/contracts/v2/workflows.ts | 4 +- .../lib/api/mcp/generated/v2-operations.ts | 4 +- apps/sim/lib/core/outbox/service.ts | 3 +- .../file-versions.integration.ts | 63 ++++---- .../workspace/workspace-file-manager.ts | 136 +++++++---------- .../workspace-file-secret-provenance.ts | 54 ++++++- .../workspace-file-storage-accounting.test.ts | 110 ++++++-------- ...kspace-file-storage-cleanup-outbox.test.ts | 20 ++- .../workspace-file-storage-cleanup-outbox.ts | 44 +++--- .../workspace/workspace-file-versions.ts | 142 +++++++++++------- apps/sim/lib/uploads/core/errors.ts | 9 +- apps/sim/lib/users/queries.ts | 16 +- .../application/file-versions.test.ts | 18 ++- .../application/file-versions.ts | 65 +++----- .../read-workspace-file-metadata.test.ts | 42 ++---- .../read-workspace-file-metadata.ts | 31 ++-- .../0365_workspace_file_versions.sql | 1 - .../db/migrations/meta/0365_snapshot.json | 6 - packages/db/schema.ts | 2 - packages/sim-cli/src/http/client.test.ts | 1 - 36 files changed, 498 insertions(+), 520 deletions(-) diff --git a/apps/docs/openapi-v2-files-audit.json b/apps/docs/openapi-v2-files-audit.json index 27fc6ad93c7..cbca66d6c92 100644 --- a/apps/docs/openapi-v2-files-audit.json +++ b/apps/docs/openapi-v2-files-audit.json @@ -868,7 +868,7 @@ "get": { "operationId": "listFileVersions", "summary": "List File Versions", - "description": "List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not, and collaborative edits or repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.", + "description": "List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.", "x-sim-operation": "files.versions.list", "x-oauth-scope": "api:read", "tags": ["Files"], @@ -2068,7 +2068,7 @@ "get": { "operationId": "getFile", "summary": "Get File Metadata", - "description": "Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.\n\nOAuth scope: `api:read`.", + "description": "Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.", "x-sim-operation": "files.read_metadata", "x-oauth-scope": "api:read", "tags": ["Files"], @@ -2145,9 +2145,6 @@ "404": { "$ref": "#/components/responses/NotFound" }, - "409": { - "$ref": "#/components/responses/Conflict" - }, "429": { "$ref": "#/components/responses/RateLimited" }, diff --git a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts index c1eae03d26d..aa37d0e95d0 100644 --- a/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts +++ b/apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts @@ -84,9 +84,8 @@ const callGet = (query: string) => const archivedFileUseCase = async ({ input }: { input: { includeDeleted?: boolean } }) => { if (!input.includeDeleted) throw new OrchestrationError('not_found', 'File not found') return { - file: { ...buildRecord(), deletedAt: new Date('2024-01-03T00:00:00Z') }, + file: { ...buildRecord(), deletedAt: new Date('2024-01-03T00:00:00Z'), currentVersion: 3 }, share: SHARE, - currentVersion: 3, } } @@ -96,7 +95,10 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { v2RouteMocks.authenticate.mockResolvedValue(auth) v2RouteMocks.preauthRate.mockResolvedValue(V2_PREAUTH_RATE_LIMIT_ALLOWED) v2RouteMocks.operationRate.mockResolvedValue(V2_OPERATION_RATE_LIMIT_ALLOWED) - mocks.readMetadata.mockResolvedValue({ file: buildRecord(), share: SHARE, currentVersion: 3 }) + mocks.readMetadata.mockResolvedValue({ + file: { ...buildRecord(), currentVersion: 3 }, + share: SHARE, + }) mocks.getUserEmailsByIds.mockResolvedValue(new Map([['user-1', 'ada@example.com']])) }) @@ -244,9 +246,8 @@ describe('GET /api/v2/files/[fileId]/metadata', () => { it('returns a null share when the file has no share configuration', async () => { mocks.readMetadata.mockResolvedValueOnce({ - file: buildRecord(), + file: { ...buildRecord(), currentVersion: 3 }, share: null, - currentVersion: 3, }) const response = await callGet(`workspaceId=${WORKSPACE_ID}`) diff --git a/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts b/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts index cf6c4ad40e8..22e14d6d20a 100644 --- a/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts +++ b/apps/sim/app/api/v2/files/[fileId]/metadata/route.ts @@ -29,7 +29,7 @@ export const GET = defineV2JsonRoute({ includeDeleted: query.scope === 'archived', }), useCase: readWorkspaceFileMetadataWithVersion, - present: async ({ file, share, currentVersion }) => ({ - data: { ...(await toV2File(file)), share, currentVersion }, + present: async ({ file, share }) => ({ + data: { ...(await toV2File(file)), share, currentVersion: file.currentVersion }, }), }) diff --git a/apps/sim/app/api/v2/files/[fileId]/text/route.ts b/apps/sim/app/api/v2/files/[fileId]/text/route.ts index ded0298ecf0..2e88aef2f18 100644 --- a/apps/sim/app/api/v2/files/[fileId]/text/route.ts +++ b/apps/sim/app/api/v2/files/[fileId]/text/route.ts @@ -3,6 +3,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/ import { v2FileErrorPolicies } from '@/lib/workspace-files/api' import { fileOperations } from '@/lib/workspace-files/application/operations' import { readWorkspaceFileText } from '@/lib/workspace-files/application/read-workspace-file-text' +import { toV2FileText } from '@/app/api/v2/files/utils' export const dynamic = 'force-dynamic' @@ -39,18 +40,5 @@ export const GET = defineV2JsonRoute({ limit: query.limit, }), useCase: readWorkspaceFileText, - present: ({ file, text, truncated, degraded, degradedReason, byteCount, lineRange }) => ({ - data: { - fileId: file.id, - name: file.name, - type: file.type, - text, - truncated, - degraded, - degradedReason, - charCount: text.length, - byteCount, - ...(lineRange ? { lineRange } : {}), - }, - }), + present: (result) => ({ data: toV2FileText(result) }), }) diff --git a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts index e7fc4d54b1a..6caf4f9e531 100644 --- a/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts +++ b/apps/sim/app/api/v2/files/[fileId]/versions/[version]/text/route.ts @@ -3,6 +3,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/ import { v2FileErrorPolicies } from '@/lib/workspace-files/api' import { readWorkspaceFileVersionText } from '@/lib/workspace-files/application/file-versions' import { fileOperations } from '@/lib/workspace-files/application/operations' +import { toV2FileText } from '@/app/api/v2/files/utils' export const dynamic = 'force-dynamic' @@ -27,28 +28,5 @@ export const GET = defineV2JsonRoute({ limit: query.limit, }), useCase: readWorkspaceFileVersionText, - present: ({ - file, - version, - text, - truncated, - degraded, - degradedReason, - byteCount, - lineRange, - }) => ({ - data: { - fileId: file.id, - version: version.version, - name: file.name, - type: version.contentType, - text, - truncated, - degraded, - degradedReason, - charCount: text.length, - byteCount, - ...(lineRange ? { lineRange } : {}), - }, - }), + present: (result) => ({ data: { ...toV2FileText(result), version: result.version.version } }), }) diff --git a/apps/sim/app/api/v2/files/utils.test.ts b/apps/sim/app/api/v2/files/utils.test.ts index 026d9b4a1c8..8af46a22b96 100644 --- a/apps/sim/app/api/v2/files/utils.test.ts +++ b/apps/sim/app/api/v2/files/utils.test.ts @@ -27,7 +27,6 @@ const version: WorkspaceFileVersionRecord = { createdAt: new Date('2026-01-02T00:00:00Z'), updatedAt: new Date('2026-01-02T00:05:00Z'), supersededAt: null, - secretProvenance: { status: 'exact', entries: [] }, } describe('toV2FileVersions', () => { diff --git a/apps/sim/app/api/v2/files/utils.ts b/apps/sim/app/api/v2/files/utils.ts index 4d37212aa7c..124050edfc8 100644 --- a/apps/sim/app/api/v2/files/utils.ts +++ b/apps/sim/app/api/v2/files/utils.ts @@ -1,5 +1,5 @@ import type { V2FileVersion } from '@/lib/api/contracts/v2/file-versions' -import type { V2File } from '@/lib/api/contracts/v2/files' +import type { V2File, V2FileText } from '@/lib/api/contracts/v2/files' import { getBaseUrl } from '@/lib/core/utils/urls' import { buildFolderPath } from '@/lib/folders/paths' import { workspaceResourceWebUrl } from '@/lib/resources' @@ -10,6 +10,7 @@ import { getUserEmailsByIds, requireResolvedUserEmail, } from '@/lib/users/queries' +import type { ReadWorkspaceFileTextResult } from '@/lib/workspace-files/application/read-workspace-file-text' import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path' /** Shared serialization for the v2 files surface. */ @@ -66,6 +67,30 @@ export async function toV2Files(records: WorkspaceFileRecord[]): Promise diff --git a/apps/sim/background/cleanup-file-versions.ts b/apps/sim/background/cleanup-file-versions.ts index 2f4b9a89ad2..9c3161b39a6 100644 --- a/apps/sim/background/cleanup-file-versions.ts +++ b/apps/sim/background/cleanup-file-versions.ts @@ -5,8 +5,11 @@ import { chunkArray } from '@sim/utils/helpers' import { task } from '@trigger.dev/sdk' import { and, count, gt, inArray, isNotNull, lt, min, or, sql } from 'drizzle-orm' import type { CleanupJobPayload } from '@/lib/billing/cleanup-dispatcher' -import type { PlanCategory } from '@/lib/billing/plan-helpers' -import { DEFAULT_DELETE_CHUNK_SIZE } from '@/lib/cleanup/batch-delete' +import { + DEFAULT_DELETE_CHUNK_SIZE, + DEFAULT_MAX_BATCHES_PER_TABLE, + DEFAULT_WORKSPACE_CHUNK_SIZE, +} from '@/lib/cleanup/batch-delete' import { retentionCleanupQueue } from '@/lib/cleanup/queue' import { enqueueWorkspaceFileStorageCleanups } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { MAX_SUPERSEDED_FILE_VERSIONS } from '@/lib/uploads/contexts/workspace/workspace-file-versions' @@ -16,31 +19,20 @@ const logger = createLogger('CleanupFileVersions') /** All cleanup queries run on the dedicated cleanup pool. */ const cleanupDb = dbFor('cleanup') -/** Workspaces whose candidate files are found in one query. */ -const WORKSPACES_PER_QUERY = 50 /** Candidate files whose histories are ranked in one query. */ const FILES_PER_QUERY = 500 -const VERSIONS_PER_BATCH = 1000 -/** Bounds one run's work per file chunk; the next daily run continues where this stopped. */ -const MAX_BATCHES_PER_CHUNK = 50 /** - * Most superseded versions a file keeps, per plan; any beyond it are pruned whatever their age. Free - * keeps its newest 100 versions (the current one included); every other plan is bounded only by the - * inline write-time ceiling. + * Superseded versions a free file keeps (its newest 100 with the current one); versions beyond it + * are pruned whatever their age. Paid plans are bounded only by the inline write-time ceiling. */ -const MAX_SUPERSEDED_VERSIONS_BY_PLAN: Record = { - free: 99, - pro: MAX_SUPERSEDED_FILE_VERSIONS, - team: MAX_SUPERSEDED_FILE_VERSIONS, - enterprise: MAX_SUPERSEDED_FILE_VERSIONS, -} - -/** Retention never prunes the newest versions of a file, whatever their age. */ -const FILE_VERSION_RETENTION_KEEP_LATEST = 10 +const FREE_MAX_SUPERSEDED_VERSIONS = 99 -/** Newest superseded versions a file keeps whatever their age (the current version is the tenth). */ -const KEEP_SUPERSEDED = FILE_VERSION_RETENTION_KEEP_LATEST - 1 +/** + * Newest superseded versions retention never prunes, whatever their age, so a file always keeps its + * newest ten versions with the current one. + */ +const KEEP_SUPERSEDED = 9 /** * Files in the group that can lose any version: more superseded versions than the keep-latest floor, @@ -79,7 +71,7 @@ async function selectCandidateFileIds( /** * Superseded versions of the given files past retention: older than the cutoff or beyond the plan's - * count, but never among the newest {@link FILE_VERSION_RETENTION_KEEP_LATEST} versions of a file. + * count, but never among the newest {@link KEEP_SUPERSEDED} superseded versions of a file. */ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: number) { const ranked = cleanupDb @@ -108,7 +100,7 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n or(lt(ranked.supersededAt, cutoff), gt(ranked.rank, maxSuperseded)) ) ) - .limit(VERSIONS_PER_BATCH) + .limit(DEFAULT_DELETE_CHUNK_SIZE) } /** @@ -116,30 +108,26 @@ function selectExpiredVersions(fileIds: string[], cutoff: Date, maxSuperseded: n * in the same transaction, so a row never outlives its release and every released object is * deleted durably — the outbox retries failures and treats an already-missing object as done. */ -async function deleteVersions(rows: Array<{ id: string }>) { - let deleted = 0 - for (const batch of chunkArray(rows, DEFAULT_DELETE_CHUNK_SIZE)) { - deleted += await cleanupDb.transaction(async (tx) => { - const removed = await tx - .delete(workspaceFileVersion) - .where( - and( - inArray( - workspaceFileVersion.id, - batch.map((row) => row.id) - ), - isNotNull(workspaceFileVersion.supersededAt) - ) +function deleteVersions(rows: Array<{ id: string }>): Promise { + return cleanupDb.transaction(async (tx) => { + const removed = await tx + .delete(workspaceFileVersion) + .where( + and( + inArray( + workspaceFileVersion.id, + rows.map((row) => row.id) + ), + isNotNull(workspaceFileVersion.supersededAt) ) - .returning({ key: workspaceFileVersion.key }) - await enqueueWorkspaceFileStorageCleanups( - tx, - removed.map((row) => row.key) ) - return removed.length - }) - } - return deleted + .returning({ key: workspaceFileVersion.key }) + await enqueueWorkspaceFileStorageCleanups( + tx, + removed.map((row) => row.key) + ) + return removed.length + }) } export async function runCleanupFileVersions(payload: CleanupJobPayload): Promise { @@ -151,21 +139,22 @@ export async function runCleanupFileVersions(payload: CleanupJobPayload): Promis } const cutoff = new Date(Date.now() - retentionHours * 60 * 60 * 1000) - const maxSuperseded = MAX_SUPERSEDED_VERSIONS_BY_PLAN[plan] + const maxSuperseded = + plan === 'free' ? FREE_MAX_SUPERSEDED_VERSIONS : MAX_SUPERSEDED_FILE_VERSIONS logger.info( `[${label}] Processing ${workspaceIds.length} workspaces, cutoff: ${cutoff.toISOString()}` ) let deleted = 0 - for (const group of chunkArray(workspaceIds, WORKSPACES_PER_QUERY)) { + for (const group of chunkArray(workspaceIds, DEFAULT_WORKSPACE_CHUNK_SIZE)) { const candidates = await selectCandidateFileIds(group, cutoff, maxSuperseded) for (const fileIds of chunkArray(candidates, FILES_PER_QUERY)) { - for (let batch = 0; batch < MAX_BATCHES_PER_CHUNK; batch++) { + for (let batch = 0; batch < DEFAULT_MAX_BATCHES_PER_TABLE; batch++) { const expired = await selectExpiredVersions(fileIds, cutoff, maxSuperseded) if (expired.length === 0) break const removed = await deleteVersions(expired) deleted += removed - if (expired.length < VERSIONS_PER_BATCH || removed === 0) break + if (expired.length < DEFAULT_DELETE_CHUNK_SIZE || removed === 0) break } } } diff --git a/apps/sim/lib/api/contracts/deployments.test.ts b/apps/sim/lib/api/contracts/deployments.test.ts index 13bc65f2d20..debe4690ac8 100644 --- a/apps/sim/lib/api/contracts/deployments.test.ts +++ b/apps/sim/lib/api/contracts/deployments.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest' import { - DEPLOYMENT_VERSION_MAX, deploymentVersionOrActiveParamsSchema, deploymentVersionParamsSchema, } from '@/lib/api/contracts/deployments' +import { INT4_MAX } from '@/lib/api/contracts/primitives' describe('deployment version route params', () => { it('coerces numeric path params from the server boundary', () => { @@ -32,12 +32,10 @@ describe('deployment version route params', () => { it.each([deploymentVersionParamsSchema, deploymentVersionOrActiveParamsSchema])( 'bounds the path version to the integer column range', (schema) => { - expect( - schema.safeParse({ id: 'workflow-1', version: String(DEPLOYMENT_VERSION_MAX) }).success - ).toBe(true) - expect( - schema.safeParse({ id: 'workflow-1', version: String(DEPLOYMENT_VERSION_MAX + 1) }).success - ).toBe(false) + expect(schema.safeParse({ id: 'workflow-1', version: String(INT4_MAX) }).success).toBe(true) + expect(schema.safeParse({ id: 'workflow-1', version: String(INT4_MAX + 1) }).success).toBe( + false + ) } ) }) diff --git a/apps/sim/lib/api/contracts/deployments.ts b/apps/sim/lib/api/contracts/deployments.ts index df68e798b97..8da43bc4a79 100644 --- a/apps/sim/lib/api/contracts/deployments.ts +++ b/apps/sim/lib/api/contracts/deployments.ts @@ -1,9 +1,5 @@ import { z } from 'zod' -import { - INT4_MAX, - versionNumberPathSchema, - versionNumberSchema, -} from '@/lib/api/contracts/primitives' +import { versionNumberPathSchema } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' import { workflowIdParamsSchema } from '@/lib/api/contracts/workflows' import { @@ -26,17 +22,6 @@ export const deployedWorkflowStateSchema = z additionalProperties: true, }) -/** - * Upper bound of `workflow_deployment_version.version`, whose column is a - * Postgres `integer`. A larger value has no row to address and overflows the - * comparison instead of missing, so every schema carrying a deployment version - * — path param, request body, or cursor payload — must be bounded by this. - */ -export const DEPLOYMENT_VERSION_MAX = INT4_MAX - -/** A deployment version number, bounded to the range its column can hold. */ -export const deploymentVersionNumberSchema = versionNumberSchema - export const deploymentVersionParamsSchema = z.object({ id: z.string().min(1, 'Invalid workflow ID'), version: versionNumberPathSchema, diff --git a/apps/sim/lib/api/contracts/primitives.ts b/apps/sim/lib/api/contracts/primitives.ts index c60f6c16be1..409f968daca 100644 --- a/apps/sim/lib/api/contracts/primitives.ts +++ b/apps/sim/lib/api/contracts/primitives.ts @@ -335,7 +335,11 @@ export const workspaceFileIdSchema = requiredFieldSchema('File ID is required') .max(MAX_ID_LENGTH, 'File ID is too long') .regex(/^[A-Za-z0-9_-]+$/, 'Invalid file id') -/** Upper bound of a Postgres `integer` column, the type every version number is stored as. */ +/** + * Upper bound of a Postgres `integer` column, the type every version number is stored as. A larger + * value has no row to address and overflows the comparison instead of missing, so every schema + * carrying a version — path param, request body, or cursor payload — must be bounded by this. + */ export const INT4_MAX = 2147483647 /** A version number in a body or cursor, bounded to the range its column can hold. */ diff --git a/apps/sim/lib/api/contracts/v1/workflows.ts b/apps/sim/lib/api/contracts/v1/workflows.ts index e055ddd9df4..c840ee37c41 100644 --- a/apps/sim/lib/api/contracts/v1/workflows.ts +++ b/apps/sim/lib/api/contracts/v1/workflows.ts @@ -3,9 +3,12 @@ import { activeDeploymentSummarySchema, deploymentOperationSummarySchema, deploymentVersionMetadataFieldsSchema, - deploymentVersionNumberSchema, } from '@/lib/api/contracts/deployments' -import { booleanQueryFlagSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { + booleanQueryFlagSchema, + versionNumberSchema, + workspaceIdSchema, +} from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' import { workflowIdParamsSchema, workflowStateSchema } from '@/lib/api/contracts/workflows' @@ -79,7 +82,7 @@ export type V1DeployWorkflowBody = z.input * `parseOptionalJsonBody`, so it is not attached to the contract. */ export const v1RollbackWorkflowBodySchema = z.object({ - version: deploymentVersionNumberSchema.optional(), + version: versionNumberSchema.optional(), }) export type V1RollbackWorkflowBody = z.input diff --git a/apps/sim/lib/api/contracts/v2/file-versions.ts b/apps/sim/lib/api/contracts/v2/file-versions.ts index 4926725818c..fdb75e2318c 100644 --- a/apps/sim/lib/api/contracts/v2/file-versions.ts +++ b/apps/sim/lib/api/contracts/v2/file-versions.ts @@ -23,9 +23,9 @@ import { /** * v2 file version history contracts. Every content write that changes the bytes — an upload, an * API or editor save, a Sim edit, a workflow write, a revert — records a version. Collaborative - * edits and repeated workflow writes from one author fold into the current version until it is ten - * minutes old or its writer has been idle for five. Renames and moves are metadata changes and never - * create versions, so every version reads under the file's current name. + * edits, and repeated workflow writes from one author, fold into the current version until it is + * ten minutes old or its writers have been idle for five. Renames and moves are metadata changes + * and never create versions, so every version reads under the file's current name. */ export const v2FileVersionSourceSchema = z diff --git a/apps/sim/lib/api/contracts/v2/logs.ts b/apps/sim/lib/api/contracts/v2/logs.ts index 6428891ad84..9915ecdd7f1 100644 --- a/apps/sim/lib/api/contracts/v2/logs.ts +++ b/apps/sim/lib/api/contracts/v2/logs.ts @@ -338,7 +338,7 @@ export const v2LogParamsSchema = z.object({ * Upper bound of `workflow_execution_logs.total_duration_ms`, whose column is a * Postgres `integer`. * - * The same rule `DEPLOYMENT_VERSION_MAX` states for deployment versions: a + * The same rule `INT4_MAX` states for version numbers: a * comparison against an `integer` column is an `integer` comparison, so a bound * outside int4 — or one carrying a fractional part — is not a filter that * matches nothing, it is a value Postgres refuses to parse. `1.5`, diff --git a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts index cc9b36665ce..c6a48d17003 100644 --- a/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts +++ b/apps/sim/lib/api/contracts/v2/openapi/files-audit.ts @@ -447,7 +447,7 @@ const declaredRoutes = [ operationId: 'listFileVersions', summary: 'List File Versions', description: - 'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not, and collaborative edits or repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.', + 'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.', errors: RESOURCE_ERRORS, success: { description: 'A page of file versions.' }, }), @@ -848,8 +848,8 @@ const declaredRoutes = [ operationId: 'getFile', summary: 'Get File Metadata', description: - 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.', - errors: RESOURCE_CONFLICT_ERRORS, + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.', + errors: RESOURCE_ERRORS, success: { description: 'File metadata and public-share state.' }, }), { diff --git a/apps/sim/lib/api/contracts/v2/workflow-deployment-requests.test.ts b/apps/sim/lib/api/contracts/v2/workflow-deployment-requests.test.ts index 28d76c57727..21c768156e5 100644 --- a/apps/sim/lib/api/contracts/v2/workflow-deployment-requests.test.ts +++ b/apps/sim/lib/api/contracts/v2/workflow-deployment-requests.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { DEPLOYMENT_VERSION_MAX } from '@/lib/api/contracts/deployments' +import { INT4_MAX } from '@/lib/api/contracts/primitives' import { v2DeployWorkflowContract, v2GetWorkflowRunContract, @@ -44,15 +44,13 @@ describe('v2 deployment request contracts', () => { }) it('rejects a rollback version past the range its column can hold', () => { - expect(rollbackBody.safeParse({ version: DEPLOYMENT_VERSION_MAX }).success).toBe(true) - expect(rollbackBody.safeParse({ version: DEPLOYMENT_VERSION_MAX + 1 }).success).toBe(false) + expect(rollbackBody.safeParse({ version: INT4_MAX }).success).toBe(true) + expect(rollbackBody.safeParse({ version: INT4_MAX + 1 }).success).toBe(false) }) it('bounds the version a forged versions cursor can carry into the query', () => { expect(v2WorkflowVersionCursorSchema.safeParse({ version: 2 }).success).toBe(true) - expect( - v2WorkflowVersionCursorSchema.safeParse({ version: DEPLOYMENT_VERSION_MAX + 1 }).success - ).toBe(false) + expect(v2WorkflowVersionCursorSchema.safeParse({ version: INT4_MAX + 1 }).success).toBe(false) expect(v2WorkflowVersionCursorSchema.safeParse({ version: 0 }).success).toBe(false) expect(v2WorkflowVersionCursorSchema.safeParse({ version: 'two' }).success).toBe(false) expect(v2WorkflowVersionCursorSchema.safeParse({}).success).toBe(false) diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 7bf93b7b7d4..705227a3b18 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -9,7 +9,6 @@ import { activeDeploymentSummarySchema, deployedWorkflowStateSchema, deploymentOperationSummarySchema, - deploymentVersionNumberSchema, deploymentVersionOrActiveParamsSchema, deploymentVersionParamsSchema, deploymentVersionSchema, @@ -21,6 +20,7 @@ import { missingFieldError, noInputSchema, runIdSchema, + versionNumberSchema, workspaceIdSchema, } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' @@ -763,7 +763,7 @@ export type V2ListWorkflowVersionsQuery = z.output diff --git a/apps/sim/lib/api/mcp/generated/v2-operations.ts b/apps/sim/lib/api/mcp/generated/v2-operations.ts index cf0060b69be..12d7f9e393b 100644 --- a/apps/sim/lib/api/mcp/generated/v2-operations.ts +++ b/apps/sim/lib/api/mcp/generated/v2-operations.ts @@ -1035,7 +1035,7 @@ export const V2_MCP_OPERATIONS = { contract: v2GetFileContract, summary: 'Get File Metadata', description: - 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts. A file rewritten continuously while it is read returns `409`; retry.\n\nOAuth scope: `api:read`.', + 'Get file metadata, its public-share configuration, and the version number of its current content. The `share` field is null when the file has never been shared. `currentVersion` identifies the content in List File Versions and is the precondition Revert File Version accepts.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/[fileId]/metadata/route').then((route) => route.GET), }, getFileShare: { @@ -1425,7 +1425,7 @@ export const V2_MCP_OPERATIONS = { contract: v2ListFileVersionsContract, summary: 'List File Versions', description: - 'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not, and collaborative edits or repeated workflow writes by one author within ten minutes fold into one. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.', + 'List the versions of a file, newest first by default. Each write that changes the bytes records one; identical rewrites do not. Collaborative edits within ten minutes fold into one version, as do repeated workflow writes by one author. Renames and moves are not versions. An empty file is version 1 until its first content replaces it. Retention keeps the newest ten and removes older versions by plan, so numbers can have gaps.\n\nOAuth scope: `api:read`.', handler: () => import('@/app/api/v2/files/[fileId]/versions/route').then((route) => route.GET), }, listKnowledgeBases: { diff --git a/apps/sim/lib/core/outbox/service.ts b/apps/sim/lib/core/outbox/service.ts index f3b857699e4..54ce6e779f4 100644 --- a/apps/sim/lib/core/outbox/service.ts +++ b/apps/sim/lib/core/outbox/service.ts @@ -10,7 +10,8 @@ import { readyEventTypesQuery } from '@/lib/core/outbox/queries' const logger = createLogger('OutboxService') const DEFAULT_MAX_ATTEMPTS = 10 -const MAX_BULK_ENQUEUE_EVENTS = 1_000 +/** Most events one {@link enqueueOutboxEvents} call may insert. */ +export const MAX_BULK_ENQUEUE_EVENTS = 1_000 const MAX_PERSISTED_ERROR_LENGTH = 500 const MAX_REAPED_EVENTS = 1_000 diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index eef4379ec78..0cfd8654cee 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -32,13 +32,13 @@ import { deleteWorkspaceFileVersion, fetchWorkspaceFileBuffer, getWorkspaceFile, + getWorkspaceFileWithCurrentVersion, updateWorkspaceFileContent, uploadWorkspaceFile, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { getCurrentWorkspaceFileVersion, - getWorkspaceFileVersionNumberForRecord, queryWorkspaceFileVersions, releaseWorkspaceFileVersionsForPurgeInTx, } from '@/lib/uploads/contexts/workspace/workspace-file-versions' @@ -228,8 +228,35 @@ describe('workspace file version history in PostgreSQL', () => { const after = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) if (!after) throw new Error('file missing') expect((await getCurrentWorkspaceFileVersion(after)).version).toBe(2) - expect(await getWorkspaceFileVersionNumberForRecord(before)).toBe(1) - expect(await getWorkspaceFileVersionNumberForRecord(after)).toBe(2) + }) + + it('reads a record with the version of the bytes it describes', async () => { + const fixture = await seedFile('original') + await expect( + getWorkspaceFileWithCurrentVersion(fixture.workspaceId, fixture.fileId) + ).resolves.toMatchObject({ key: fixture.firstKey, currentVersion: 1 }) + + const write = { source: 'collab', authorUserId: fixture.aliceId } as const + await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('draft one'), + undefined, + { version: write } + ) + const coalesced = await updateWorkspaceFileContent( + fixture.workspaceId, + fixture.fileId, + fixture.aliceId, + Buffer.from('draft two'), + undefined, + { version: write } + ) + + await expect( + getWorkspaceFileWithCurrentVersion(fixture.workspaceId, fixture.fileId) + ).resolves.toMatchObject({ key: coalesced.key, currentVersion: 2 }) }) it('does not keep an empty shell as a version of its own', async () => { @@ -271,34 +298,6 @@ describe('workspace file version history in PostgreSQL', () => { expect(await objectExists(fixture.firstKey)).toBe(false) }) - it('reports a stale record as unresolvable once a coalesced write released its key', async () => { - const fixture = await seedFile('original') - const write = { source: 'collab', authorUserId: fixture.aliceId } as const - await updateWorkspaceFileContent( - fixture.workspaceId, - fixture.fileId, - fixture.aliceId, - Buffer.from('draft one'), - undefined, - { version: write } - ) - const stale = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) - if (!stale) throw new Error('file missing') - await updateWorkspaceFileContent( - fixture.workspaceId, - fixture.fileId, - fixture.aliceId, - Buffer.from('draft two'), - undefined, - { version: write } - ) - const fresh = await getWorkspaceFile(fixture.workspaceId, fixture.fileId) - if (!fresh) throw new Error('file missing') - - expect(await getWorkspaceFileVersionNumberForRecord(stale)).toBeNull() - expect(await getWorkspaceFileVersionNumberForRecord(fresh)).toBe(2) - }) - it('never folds deliberate writes, and repoints the head for identical bytes', async () => { const fixture = await seedFile('original') const write = { source: 'api', authorUserId: fixture.aliceId } as const @@ -385,7 +384,7 @@ describe('workspace file version history in PostgreSQL', () => { input: { fileId: fixture.fileId, assertedWorkspaceId: fixture.workspaceId, version: 2 }, }) - expect(result).toMatchObject({ reverted: true, revertedFrom: 3, revertedTo: 2 }) + expect(result).toMatchObject({ reverted: true, revertedFrom: 3 }) expect(result.version).toMatchObject({ version: 4, source: 'revert', diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 89bce9b4389..2d49bf200ec 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -5,14 +5,9 @@ import { randomBytes } from 'crypto' import { db } from '@sim/db' -import { - uploadSession, - type WorkspaceFileRow, - workspace, - workspaceFiles, - workspaceFileVersion, -} from '@sim/db/schema' +import { uploadSession, type WorkspaceFileRow, workspace, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { describeError, getErrorMessage, @@ -68,24 +63,23 @@ import { processWorkspaceFileLiveDocReconciliationNow, } from '@/lib/uploads/contexts/workspace/workspace-file-live-doc-outbox' import { + applyWorkspaceFileSecretProvenancePolicyInTx, EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE, initializeWorkspaceFileSecretProvenanceInTx, - preserveWorkspaceFileSecretProvenanceInTx, - reinstateWorkspaceFileSecretProvenanceInTx, replaceWorkspaceFileSecretProvenanceInTx, snapshotWorkspaceFileSecretProvenanceInTx, type WorkspaceFileSecretProvenance, type WorkspaceFileSecretProvenancePolicy, } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' import { - enqueueWorkspaceFileStorageCleanup, enqueueWorkspaceFileStorageCleanups, processWorkspaceFileStorageCleanupsNow, } from '@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox' import { + currentWorkspaceFileVersionNumberSql, deleteWorkspaceFileVersionInTx, - hashWorkspaceFileContent, isVersionHeadCurrent, + listWorkspaceFileVersionKeysInTx, loadWorkspaceFileVersionHead, recordWorkspaceFileVersionInTx, type WorkspaceFileVersionWrite, @@ -1660,18 +1654,10 @@ export async function getWorkspaceFile( .select() .from(workspaceFiles) .where( - includeDeleted - ? and( - eq(workspaceFiles.id, fileId), - eq(workspaceFiles.workspaceId, workspaceId), - eq(workspaceFiles.context, 'workspace') - ) - : and( - eq(workspaceFiles.id, fileId), - eq(workspaceFiles.workspaceId, workspaceId), - eq(workspaceFiles.context, 'workspace'), - isNull(workspaceFiles.deletedAt) - ) + and( + eq(workspaceFiles.id, fileId), + workspaceFileScopeCondition(workspaceId, includeDeleted ? 'all' : 'active') + ) ) .limit(1) @@ -1685,6 +1671,35 @@ export async function getWorkspaceFile( } } +/** + * {@link getWorkspaceFile} plus the number of the version its record describes, read in one + * statement so a concurrent write can never pair this record with another write's version. + */ +export async function getWorkspaceFileWithCurrentVersion( + workspaceId: string, + fileId: string, + options?: { includeDeleted?: boolean } +): Promise<(WorkspaceFileRecord & { currentVersion: number }) | null> { + const [row] = await db + .select({ + file: workspaceFiles, + currentVersion: currentWorkspaceFileVersionNumberSql(), + }) + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.id, fileId), + workspaceFileScopeCondition(workspaceId, options?.includeDeleted ? 'all' : 'active') + ) + ) + .limit(1) + if (!row) return null + return { + ...(await mapSingleWorkspaceFileRecord(row.file, workspaceId)), + currentVersion: row.currentVersion, + } +} + /** * Download the bytes a user should actually receive for a workspace file. * @@ -1819,7 +1834,7 @@ export async function updateWorkspaceFileContent( const storageBillingContext = await resolveStorageBillingContext(workspaceId) const nextContentType = contentType || fileRecord.type const nextStorageKey = generateWorkspaceFileKey(workspaceId, fileRecord.name) - const contentHash = hashWorkspaceFileContent(content) + const contentHash = sha256Hex(content) try { const metadata: Record = { @@ -1937,51 +1952,20 @@ export async function updateWorkspaceFileContent( throw new OrchestrationError('not_found', 'File not found or could not be updated') } - if (options.secretProvenancePolicy?.mode === 'replace') { - await replaceWorkspaceFileSecretProvenanceInTx( - tx, - fileId, - updatedFile.contentUpdatedAt, - options.secretProvenancePolicy.provenance - ) - } else if (options.secretProvenancePolicy?.mode === 'reinstate') { - await reinstateWorkspaceFileSecretProvenanceInTx( - tx, - fileId, - updatedFile.contentUpdatedAt, - options.secretProvenancePolicy.snapshot - ) - } else if (options.secretProvenancePolicy?.mode === 'preserve') { - await preserveWorkspaceFileSecretProvenanceInTx( - tx, - fileId, - currentFile.contentUpdatedAt, - currentFile.secretProvenanceVersion, - updatedFile.contentUpdatedAt - ) - } else { - await replaceWorkspaceFileSecretProvenanceInTx(tx, fileId, updatedFile.contentUpdatedAt, { - status: 'unknown', - }) - } - - /** Every policy but `preserve` binds a sidecar, which marks the file tracked. */ - const secretProvenanceVersion = - options.secretProvenancePolicy?.mode === 'preserve' - ? currentFile.secretProvenanceVersion - : 1 + const nextProvenance = await applyWorkspaceFileSecretProvenancePolicyInTx( + tx, + fileId, + currentFile, + updatedFile.contentUpdatedAt, + options.secretProvenancePolicy + ) const recorded = await recordWorkspaceFileVersionInTx(tx, { workspaceId, head: versionHead, previous: currentFile, previousProvenance, next: updatedFile, - nextProvenance: await snapshotWorkspaceFileSecretProvenanceInTx( - tx, - fileId, - updatedFile.contentUpdatedAt, - secretProvenanceVersion - ), + nextProvenance, contentHash, write: options.version, now, @@ -2112,7 +2096,7 @@ export async function updateWorkspaceFileContent( /** * Deletes one superseded version of an active workspace file and releases its stored object. The - * file row is locked so the delete serializes with content writes that renumber or prune history. + * file row is locked so the delete serializes with content writes that supersede or prune history. * Returns false when the file has no such superseded version (it never existed, retention removed * it, or it is the current version). */ @@ -2121,27 +2105,20 @@ export async function deleteWorkspaceFileVersion( fileId: string, version: number ): Promise { - const cleanupEventId = await db.transaction(async (tx) => { + const cleanupEventIds = await db.transaction(async (tx) => { const [file] = await tx .select({ id: workspaceFiles.id }) .from(workspaceFiles) - .where( - and( - eq(workspaceFiles.id, fileId), - eq(workspaceFiles.workspaceId, workspaceId), - eq(workspaceFiles.context, 'workspace'), - isNull(workspaceFiles.deletedAt) - ) - ) + .where(and(eq(workspaceFiles.id, fileId), workspaceFileScopeCondition(workspaceId, 'active'))) .for('update') .limit(1) if (!file) throw new OrchestrationError('not_found', 'File not found') const key = await deleteWorkspaceFileVersionInTx(tx, fileId, version) - return key ? enqueueWorkspaceFileStorageCleanup(tx, { key }) : null + return key ? enqueueWorkspaceFileStorageCleanups(tx, [key]) : null }) - if (!cleanupEventId) return false + if (!cleanupEventIds) return false - await processWorkspaceFileStorageCleanupsNow([cleanupEventId], { + await processWorkspaceFileStorageCleanupsNow(cleanupEventIds, { workspaceId, fileId, reason: 'deleted version', @@ -2359,10 +2336,7 @@ export async function purgeCreatedWorkspaceFile(params: { .limit(1) if (!lockedFile) return null - const versionKeys = await tx - .select({ key: workspaceFileVersion.key }) - .from(workspaceFileVersion) - .where(eq(workspaceFileVersion.fileId, lockedFile.id)) + const versionKeys = await listWorkspaceFileVersionKeysInTx(tx, lockedFile.id) const [deleted] = await tx .delete(workspaceFiles) @@ -2375,7 +2349,7 @@ export async function purgeCreatedWorkspaceFile(params: { storageBillingContext, getWorkspaceFileSize(lockedFile) ) - const keys = new Set([lockedFile.key, ...versionKeys.map((row) => row.key)]) + const keys = new Set([lockedFile.key, ...versionKeys]) return enqueueWorkspaceFileStorageCleanups(tx, [...keys]) }) if (!cleanupEventIds) return false diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts index ef23bd9aef5..6d55219628a 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts @@ -622,7 +622,7 @@ export async function snapshotWorkspaceFileSecretProvenanceInTx( * verbatim so a revert restores exactly the classification its bytes carried. An untracked snapshot * binds as exact-empty — the reading an untracked marker already produced for those bytes. */ -export async function reinstateWorkspaceFileSecretProvenanceInTx( +async function reinstateWorkspaceFileSecretProvenanceInTx( tx: DbTransaction, fileId: string, contentUpdatedAt: Date, @@ -639,6 +639,58 @@ export async function reinstateWorkspaceFileSecretProvenanceInTx( await replaceWorkspaceFileSecretProvenanceInTx(tx, fileId, contentUpdatedAt, provenance) } +/** + * Binds a content write's provenance to its new content version as `policy` directs — no policy + * records the bytes as unknown — and returns the snapshot now bound, read back so a version records + * exactly what a reader of those bytes sees. `previous` is the file row before the write. + */ +export async function applyWorkspaceFileSecretProvenancePolicyInTx( + tx: DbTransaction, + fileId: string, + previous: { contentUpdatedAt: Date; secretProvenanceVersion: number | null }, + nextContentUpdatedAt: Date, + policy: WorkspaceFileSecretProvenancePolicy | undefined +): Promise { + switch (policy?.mode) { + case 'replace': + await replaceWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + nextContentUpdatedAt, + policy.provenance + ) + break + case 'reinstate': + await reinstateWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + nextContentUpdatedAt, + policy.snapshot + ) + break + case 'preserve': + await preserveWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + previous.contentUpdatedAt, + previous.secretProvenanceVersion, + nextContentUpdatedAt + ) + /** An untracked file stays untracked; every other outcome binds a sidecar. */ + return snapshotWorkspaceFileSecretProvenanceInTx( + tx, + fileId, + nextContentUpdatedAt, + previous.secretProvenanceVersion + ) + default: + await replaceWorkspaceFileSecretProvenanceInTx(tx, fileId, nextContentUpdatedAt, { + status: 'unknown', + }) + } + return snapshotWorkspaceFileSecretProvenanceInTx(tx, fileId, nextContentUpdatedAt, 1) +} + /** Copies exact provenance for a byte-identical, same-owner-scope file copy; otherwise unknown. */ export async function copyWorkspaceFileSecretProvenanceInTx( tx: DbTransaction, diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts index a03eea9ef9e..c0769740772 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-accounting.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { workspaceFiles } from '@sim/db/schema' +import { sha256Hex } from '@sim/security/hash' import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' import { describeError } from '@sim/utils/errors' import { PASTE_LIMITS } from '@sim/utils/paste' @@ -11,7 +12,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockDecrementStorageUsageForBillingContextInTx, mockDeleteFile, - mockEnqueueWorkspaceFileStorageCleanup, + mockEnqueueWorkspaceFileStorageCleanups, mockEnqueueWorkspaceFileLiveDocReconciliation, mockGetWorkspaceWithOwner, mockHasCloudStorage, @@ -23,7 +24,8 @@ const { mockInitializeWorkspaceFileSecretProvenanceInTx, mockMaybeNotifyStorageLimitForBillingContext, mockNotifyWorkspaceFilesChanged, - mockProcessWorkspaceFileStorageCleanupNow, + mockProcessWorkspaceFileStorageCleanupsNow, + mockApplyWorkspaceFileSecretProvenancePolicyInTx, mockProcessWorkspaceFileLiveDocReconciliationNow, mockResolveStorageBillingContext, mockResolveFolderPathFromIndex, @@ -35,7 +37,7 @@ const { } = vi.hoisted(() => ({ mockDecrementStorageUsageForBillingContextInTx: vi.fn(), mockDeleteFile: vi.fn(), - mockEnqueueWorkspaceFileStorageCleanup: vi.fn(), + mockEnqueueWorkspaceFileStorageCleanups: vi.fn(), mockEnqueueWorkspaceFileLiveDocReconciliation: vi.fn(), mockGetWorkspaceWithOwner: vi.fn(), mockHasCloudStorage: vi.fn(), @@ -47,7 +49,8 @@ const { mockInitializeWorkspaceFileSecretProvenanceInTx: vi.fn(), mockMaybeNotifyStorageLimitForBillingContext: vi.fn(), mockNotifyWorkspaceFilesChanged: vi.fn(), - mockProcessWorkspaceFileStorageCleanupNow: vi.fn(), + mockProcessWorkspaceFileStorageCleanupsNow: vi.fn(), + mockApplyWorkspaceFileSecretProvenancePolicyInTx: vi.fn(), mockProcessWorkspaceFileLiveDocReconciliationNow: vi.fn(), mockResolveStorageBillingContext: vi.fn(), mockResolveFolderPathFromIndex: vi.fn(), @@ -66,15 +69,14 @@ vi.mock('@/lib/collab-doc/collab-state', () => ({ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', () => ({ EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE: { status: 'exact', entries: [] }, initializeWorkspaceFileSecretProvenanceInTx: mockInitializeWorkspaceFileSecretProvenanceInTx, - preserveWorkspaceFileSecretProvenanceInTx: vi.fn(), - reinstateWorkspaceFileSecretProvenanceInTx: vi.fn(), + applyWorkspaceFileSecretProvenancePolicyInTx: mockApplyWorkspaceFileSecretProvenancePolicyInTx, replaceWorkspaceFileSecretProvenanceInTx: mockReplaceWorkspaceFileSecretProvenanceInTx, snapshotWorkspaceFileSecretProvenanceInTx: vi.fn(async () => ({ status: 'exact', entries: [] })), })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ - hashWorkspaceFileContent: vi.fn(() => 'content-hash'), isVersionHeadCurrent: vi.fn(() => false), + listWorkspaceFileVersionKeysInTx: vi.fn(async () => []), loadWorkspaceFileVersionHead: vi.fn(async () => undefined), recordWorkspaceFileVersionInTx: mockRecordWorkspaceFileVersionInTx, })) @@ -108,15 +110,8 @@ vi.mock('@/lib/uploads/core/storage-service', () => ({ })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox', () => ({ - enqueueWorkspaceFileStorageCleanup: mockEnqueueWorkspaceFileStorageCleanup, - enqueueWorkspaceFileStorageCleanups: (executor: unknown, keys: string[]) => - Promise.all(keys.map((key) => mockEnqueueWorkspaceFileStorageCleanup(executor, { key }))), - processWorkspaceFileStorageCleanupNow: mockProcessWorkspaceFileStorageCleanupNow, - processWorkspaceFileStorageCleanupsNow: async (eventIds: string[]) => { - for (const eventId of eventIds) { - await mockProcessWorkspaceFileStorageCleanupNow(eventId).catch(() => undefined) - } - }, + enqueueWorkspaceFileStorageCleanups: mockEnqueueWorkspaceFileStorageCleanups, + processWorkspaceFileStorageCleanupsNow: mockProcessWorkspaceFileStorageCleanupsNow, })) vi.mock('@/lib/uploads/contexts/workspace/workspace-file-folder-manager', () => ({ @@ -203,10 +198,16 @@ describe('workspace file metadata and storage accounting', () => { mockDecrementStorageUsageForBillingContextInTx.mockResolvedValue(undefined) mockMaybeNotifyStorageLimitForBillingContext.mockResolvedValue(undefined) mockDeleteFile.mockResolvedValue(undefined) - mockEnqueueWorkspaceFileStorageCleanup.mockResolvedValue('cleanup-event-1') + mockEnqueueWorkspaceFileStorageCleanups.mockImplementation(async (_executor, keys: string[]) => + keys.map(() => 'cleanup-event-1') + ) mockEnqueueWorkspaceFileLiveDocReconciliation.mockResolvedValue('live-doc-event-1') mockNotifyWorkspaceFilesChanged.mockResolvedValue(undefined) - mockProcessWorkspaceFileStorageCleanupNow.mockResolvedValue('completed') + mockProcessWorkspaceFileStorageCleanupsNow.mockResolvedValue(undefined) + mockApplyWorkspaceFileSecretProvenancePolicyInTx.mockResolvedValue({ + status: 'unknown', + entries: [], + }) mockProcessWorkspaceFileLiveDocReconciliationNow.mockResolvedValue('completed') mockReplaceWorkspaceFileSecretProvenanceInTx.mockResolvedValue(undefined) mockSaveCollabDocStateInTx.mockResolvedValue(undefined) @@ -342,16 +343,19 @@ describe('workspace file metadata and storage accounting', () => { STORAGE_CONTEXT, FILE_ROW.size ) - expect(mockEnqueueWorkspaceFileStorageCleanup).toHaveBeenCalledWith(expect.any(Object), { - key: FILE_ROW.key, - }) + expect(mockEnqueueWorkspaceFileStorageCleanups).toHaveBeenCalledWith(expect.any(Object), [ + FILE_ROW.key, + ]) expect(dbChainMockFns.delete.mock.invocationCallOrder[0]).toBeLessThan( mockDecrementStorageUsageForBillingContextInTx.mock.invocationCallOrder[0] ) expect(mockDecrementStorageUsageForBillingContextInTx.mock.invocationCallOrder[0]).toBeLessThan( - mockEnqueueWorkspaceFileStorageCleanup.mock.invocationCallOrder[0] + mockEnqueueWorkspaceFileStorageCleanups.mock.invocationCallOrder[0] + ) + expect(mockProcessWorkspaceFileStorageCleanupsNow).toHaveBeenCalledWith( + ['cleanup-event-1'], + expect.any(Object) ) - expect(mockProcessWorkspaceFileStorageCleanupNow).toHaveBeenCalledWith('cleanup-event-1') expect(mockDeleteFile).not.toHaveBeenCalled() }) @@ -370,8 +374,8 @@ describe('workspace file metadata and storage accounting', () => { ).resolves.toBe(false) expect(mockDecrementStorageUsageForBillingContextInTx).not.toHaveBeenCalled() - expect(mockEnqueueWorkspaceFileStorageCleanup).not.toHaveBeenCalled() - expect(mockProcessWorkspaceFileStorageCleanupNow).not.toHaveBeenCalled() + expect(mockEnqueueWorkspaceFileStorageCleanups).not.toHaveBeenCalled() + expect(mockProcessWorkspaceFileStorageCleanupsNow).not.toHaveBeenCalled() expect(mockDeleteFile).not.toHaveBeenCalled() }) @@ -394,32 +398,8 @@ describe('workspace file metadata and storage accounting', () => { }) ).rejects.toThrow('accounting unavailable') - expect(mockEnqueueWorkspaceFileStorageCleanup).not.toHaveBeenCalled() - expect(mockProcessWorkspaceFileStorageCleanupNow).not.toHaveBeenCalled() - expect(mockDeleteFile).not.toHaveBeenCalled() - }) - - it('keeps deferred cleanup durable when immediate processing fails', async () => { - const extractedRow = { ...FILE_ROW, folderId: 'folder-archive' } - dbChainMockFns.limit.mockResolvedValueOnce([extractedRow]) - dbChainMockFns.returning.mockResolvedValueOnce([extractedRow]) - mockProcessWorkspaceFileStorageCleanupNow.mockRejectedValueOnce( - new Error('outbox processor unavailable') - ) - - await expect( - purgeCreatedWorkspaceFile({ - workspaceId: FILE_ROW.workspaceId, - fileId: FILE_ROW.id, - key: FILE_ROW.key, - expectedName: FILE_ROW.originalName, - expectedFolderId: extractedRow.folderId, - expectedUpdatedAt: FILE_ROW.updatedAt, - }) - ).resolves.toBe(true) - - expect(mockEnqueueWorkspaceFileStorageCleanup).toHaveBeenCalledOnce() - expect(mockProcessWorkspaceFileStorageCleanupNow).toHaveBeenCalledWith('cleanup-event-1') + expect(mockEnqueueWorkspaceFileStorageCleanups).not.toHaveBeenCalled() + expect(mockProcessWorkspaceFileStorageCleanupsNow).not.toHaveBeenCalled() expect(mockDeleteFile).not.toHaveBeenCalled() }) @@ -741,23 +721,25 @@ describe('workspace file metadata and storage accounting', () => { STORAGE_CONTEXT, 3 ) - expect(mockReplaceWorkspaceFileSecretProvenanceInTx).toHaveBeenCalledWith( + expect(mockApplyWorkspaceFileSecretProvenancePolicyInTx).toHaveBeenCalledWith( expect.any(Object), FILE_ROW.id, + concurrentFile, updatedFile.contentUpdatedAt, - { status: 'unknown' } + undefined ) expect(mockRecordWorkspaceFileVersionInTx).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ previous: concurrentFile, next: updatedFile, - contentHash: 'content-hash', + nextProvenance: { status: 'unknown', entries: [] }, + contentHash: sha256Hex(Buffer.alloc(10)), write: TEST_VERSION_WRITE, }) ) expect(mockDeleteFile).not.toHaveBeenCalled() - expect(mockEnqueueWorkspaceFileStorageCleanup).not.toHaveBeenCalled() + expect(mockEnqueueWorkspaceFileStorageCleanups).toHaveBeenCalledWith(expect.any(Object), []) expect(updated.key).toBe(replacementKey) expect(mockUploadFile.mock.invocationCallOrder[0]).toBeLessThan( dbChainMockFns.transaction.mock.invocationCallOrder[0] @@ -771,16 +753,15 @@ describe('workspace file metadata and storage accounting', () => { let committed = false dbChainMockFns.transaction.mockImplementationOnce(async (callback) => { const result = await callback(transaction) - expect(mockEnqueueWorkspaceFileStorageCleanup).toHaveBeenCalledWith(transaction, { - key: FILE_ROW.key, - }) - expect(mockProcessWorkspaceFileStorageCleanupNow).not.toHaveBeenCalled() + expect(mockEnqueueWorkspaceFileStorageCleanups).toHaveBeenCalledWith(transaction, [ + FILE_ROW.key, + ]) + expect(mockProcessWorkspaceFileStorageCleanupsNow).not.toHaveBeenCalled() committed = true return result }) - mockProcessWorkspaceFileStorageCleanupNow.mockImplementationOnce(async () => { + mockProcessWorkspaceFileStorageCleanupsNow.mockImplementationOnce(async () => { expect(committed).toBe(true) - return 'completed' }) dbChainMockFns.limit.mockResolvedValueOnce([FILE_ROW]).mockResolvedValueOnce([FILE_ROW]) dbChainMockFns.returning.mockResolvedValueOnce([updatedFile]) @@ -799,7 +780,10 @@ describe('workspace file metadata and storage accounting', () => { { version: { source: 'collab', authorUserId: FILE_ROW.userId } } ) - expect(mockProcessWorkspaceFileStorageCleanupNow).toHaveBeenCalledWith('cleanup-event-1') + expect(mockProcessWorkspaceFileStorageCleanupsNow).toHaveBeenCalledWith( + ['cleanup-event-1'], + expect.any(Object) + ) expect(mockDeleteFile).not.toHaveBeenCalled() }) @@ -935,7 +919,7 @@ describe('workspace file metadata and storage accounting', () => { expect(mockSaveCollabDocStateInTx).toHaveBeenCalledWith(transaction, MD_ROW.id, preparedState) expect(dbChainMockFns.update).not.toHaveBeenCalled() - expect(mockReplaceWorkspaceFileSecretProvenanceInTx).not.toHaveBeenCalled() + expect(mockApplyWorkspaceFileSecretProvenancePolicyInTx).not.toHaveBeenCalled() expect(mockIncrementStorageUsageForBillingContextInTx).not.toHaveBeenCalled() expect(mockDecrementStorageUsageForBillingContextInTx).not.toHaveBeenCalled() expect(mockEnqueueWorkspaceFileLiveDocReconciliation).not.toHaveBeenCalled() diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts index b6afaa441c4..906364f3729 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.test.ts @@ -10,8 +10,8 @@ const { mockDeleteFile, mockEnqueueOutboxEvents, mockProcessOutboxEventById } = })) vi.mock('@/lib/core/outbox/service', () => ({ - enqueueOutboxEvent: vi.fn(), enqueueOutboxEvents: mockEnqueueOutboxEvents, + MAX_BULK_ENQUEUE_EVENTS: 2, processOutboxEventById: mockProcessOutboxEventById, })) @@ -88,7 +88,7 @@ describe('batched workspace file storage cleanup', () => { vi.clearAllMocks() }) - it('enqueues every released key in one insert', async () => { + it('enqueues released keys within the bulk limit in one insert', async () => { const executor = { insert: vi.fn() } mockEnqueueOutboxEvents.mockResolvedValueOnce(['event-a', 'event-b']) @@ -102,6 +102,22 @@ describe('batched workspace file storage cleanup', () => { ) }) + it('splits keys beyond the outbox bulk limit into several inserts', async () => { + const executor = { insert: vi.fn() } + mockEnqueueOutboxEvents.mockResolvedValueOnce(['event-a', 'event-b']) + mockEnqueueOutboxEvents.mockResolvedValueOnce(['event-c']) + + await expect( + enqueueWorkspaceFileStorageCleanups(executor as never, ['a', 'b', 'c']) + ).resolves.toEqual(['event-a', 'event-b', 'event-c']) + expect(mockEnqueueOutboxEvents).toHaveBeenNthCalledWith( + 2, + executor, + WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, + [{ key: 'c' }] + ) + }) + it('processes every event and leaves failures to the outbox worker without throwing', async () => { mockProcessOutboxEventById .mockRejectedValueOnce(new Error('storage unavailable')) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts index 49a0f24a53b..b5095577877 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-storage-cleanup-outbox.ts @@ -1,9 +1,10 @@ import type { db } from '@sim/db' import { createLogger } from '@sim/logger' import { describeError, getErrorMessage } from '@sim/utils/errors' +import { chunkArray } from '@sim/utils/helpers' import { - enqueueOutboxEvent, enqueueOutboxEvents, + MAX_BULK_ENQUEUE_EVENTS, type OutboxHandler, type OutboxHandlerRegistry, processOutboxEventById, @@ -44,29 +45,25 @@ export const workspaceFileStorageCleanupOutboxHandlers = { [WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT]: cleanupWorkspaceFileStorage, } satisfies OutboxHandlerRegistry -/** Enqueues storage deletion in the transaction that removes the corresponding metadata. */ -export function enqueueWorkspaceFileStorageCleanup( - executor: Pick, - payload: WorkspaceFileStorageCleanupPayload -): Promise { - return enqueueOutboxEvent(executor, WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, payload) -} - -/** Attempts a newly committed cleanup immediately; the outbox worker retries incomplete work. */ -export function processWorkspaceFileStorageCleanupNow(eventId: string) { - return processOutboxEventById(eventId, workspaceFileStorageCleanupOutboxHandlers) -} - -/** Enqueues deletion of several storage objects in one insert, inside the releasing transaction. */ -export function enqueueWorkspaceFileStorageCleanups( +/** + * Enqueues deletion of storage objects inside the transaction that releases them, split into inserts + * the outbox accepts so a caller can release any number of keys at once. + */ +export async function enqueueWorkspaceFileStorageCleanups( executor: Pick, keys: readonly string[] ): Promise { - return enqueueOutboxEvents( - executor, - WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, - keys.map((key) => ({ key })) - ) + const eventIds: string[] = [] + for (const chunk of chunkArray([...keys], MAX_BULK_ENQUEUE_EVENTS)) { + eventIds.push( + ...(await enqueueOutboxEvents( + executor, + WORKSPACE_FILE_STORAGE_CLEANUP_OUTBOX_EVENT, + chunk.map((key): WorkspaceFileStorageCleanupPayload => ({ key })) + )) + ) + } + return eventIds } /** @@ -79,7 +76,10 @@ export async function processWorkspaceFileStorageCleanupsNow( ): Promise { for (const eventId of eventIds) { try { - const result = await processWorkspaceFileStorageCleanupNow(eventId) + const result = await processOutboxEventById( + eventId, + workspaceFileStorageCleanupOutboxHandlers + ) if (result !== 'completed') { logger.warn('Storage cleanup deferred to outbox retry', { ...logContext, eventId, result }) } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts index 814d8574c1b..5b960628378 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-versions.ts @@ -6,9 +6,8 @@ import { workspaceFiles, workspaceFileVersion, } from '@sim/db/schema' -import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' -import { and, desc, eq, inArray, isNotNull, lt, notInArray } from 'drizzle-orm' +import { and, desc, eq, inArray, isNotNull, lt, notInArray, sql } from 'drizzle-orm' import { type CursorKey, type KeysetKey, @@ -60,22 +59,39 @@ export interface WorkspaceFileVersionWrite { restoredFromVersion?: number } -/** sha256 (hex) of stored bytes — the identity used to detect rewrites of identical content. */ -export function hashWorkspaceFileContent(content: Buffer): string { - return sha256Hex(content) -} - /** - * The newest version row of a file. A writer reads it inside its transaction under the file row's - * FOR UPDATE lock; a reader reads it on its own so it never pairs rows from after a concurrent first - * write with a file record from before it. + * Every version column except the stored provenance entries, which can be large and are read only + * when a revert reinstates them. */ +const versionSummaryColumns = { + id: workspaceFileVersion.id, + fileId: workspaceFileVersion.fileId, + version: workspaceFileVersion.version, + key: workspaceFileVersion.key, + sizeBytes: workspaceFileVersion.sizeBytes, + contentType: workspaceFileVersion.contentType, + contentHash: workspaceFileVersion.contentHash, + supersededAt: workspaceFileVersion.supersededAt, + source: workspaceFileVersion.source, + authorUserIds: workspaceFileVersion.authorUserIds, + restoredFromVersion: workspaceFileVersion.restoredFromVersion, + createdAt: workspaceFileVersion.createdAt, + updatedAt: workspaceFileVersion.updatedAt, +} + +/** A version row without its provenance snapshot. */ +export type WorkspaceFileVersionSummaryRow = Omit< + WorkspaceFileVersionRow, + 'workspaceId' | 'secretProvenanceStatus' | 'secretProvenanceEntries' +> + +/** The newest version row of a file; a writer must read it under the file row's FOR UPDATE lock. */ export async function loadWorkspaceFileVersionHead( fileId: string, executor: DbOrTx = db -): Promise { +): Promise { const [head] = await executor - .select() + .select(versionSummaryColumns) .from(workspaceFileVersion) .where(eq(workspaceFileVersion.fileId, fileId)) .orderBy(desc(workspaceFileVersion.version)) @@ -89,7 +105,7 @@ export async function loadWorkspaceFileVersionHead( * version before the new one is recorded, so the caller snapshots their provenance first. */ export function isVersionHeadCurrent( - head: WorkspaceFileVersionRow | undefined, + head: WorkspaceFileVersionSummaryRow | undefined, file: Pick ): boolean { return head !== undefined && head.supersededAt === null && head.key === file.key @@ -106,7 +122,7 @@ function isOriginalUploadContent( } function canCoalesce( - head: WorkspaceFileVersionRow, + head: WorkspaceFileVersionSummaryRow, write: WorkspaceFileVersionWrite, now: Date ): boolean { @@ -131,7 +147,6 @@ function contentColumns(file: WorkspaceFileRow, provenance: WorkspaceFileSecretP key: file.key, sizeBytes: getWorkspaceFileSize(file), contentType: file.contentType, - contentUpdatedAt: file.contentUpdatedAt, secretProvenanceStatus: provenance.status, secretProvenanceEntries: provenance.entries, } @@ -148,10 +163,13 @@ interface RecordWorkspaceFileVersionParams { /** The workspace the write was scoped to; the file row's column is nullable for other contexts. */ workspaceId: string /** Head row loaded before the file row was updated. */ - head: WorkspaceFileVersionRow | undefined + head: WorkspaceFileVersionSummaryRow | undefined /** The file row as it was before this write (locked). */ previous: WorkspaceFileRow - /** Provenance of `previous`'s bytes, captured before the provenance step; required when the head is not current. */ + /** + * Provenance of `previous`'s bytes, captured before the provenance step; required when the head is + * not current. + */ previousProvenance: WorkspaceFileSecretProvenanceSnapshot | undefined /** The file row after this write. */ next: WorkspaceFileRow @@ -209,7 +227,7 @@ export async function recordWorkspaceFileVersionInTx( createdAt: previous.contentUpdatedAt, updatedAt: previous.contentUpdatedAt, }) - .returning() + .returning(versionSummaryColumns) head = materialized } @@ -239,7 +257,7 @@ export async function recordWorkspaceFileVersionInTx( const version = head.version + 1 await insertVersion(tx, params, version) - /** Version numbers are dense per file, so a file cannot exceed the ceiling before this number. */ + /** Numbers are assigned consecutively, so a file cannot exceed the ceiling before this number. */ const releasedKeys = version > MAX_SUPERSEDED_FILE_VERSIONS + 1 ? await pruneExcessWorkspaceFileVersionsInTx(tx, next.id) @@ -290,9 +308,6 @@ export async function deleteWorkspaceFileVersionInTx( return deleted?.key ?? null } -/** Most cleanup events one outbox insert may carry. */ -const RELEASE_ENQUEUE_CHUNK_SIZE = 1000 - /** * Releases the history of soft-deleted files inside the transaction that purges their rows: it * locks the files still deleted before `deletedBefore`, deletes their superseded version rows, and @@ -334,12 +349,10 @@ export async function releaseWorkspaceFileVersionsForPurgeInTx( ) ) .returning({ key: workspaceFileVersion.key }) - for (let offset = 0; offset < released.length; offset += RELEASE_ENQUEUE_CHUNK_SIZE) { - await enqueueWorkspaceFileStorageCleanups( - tx, - released.slice(offset, offset + RELEASE_ENQUEUE_CHUNK_SIZE).map((row) => row.key) - ) - } + await enqueueWorkspaceFileStorageCleanups( + tx, + released.map((row) => row.key) + ) } /** Deletes superseded versions beyond {@link MAX_SUPERSEDED_FILE_VERSIONS}, returning their keys. */ @@ -388,7 +401,6 @@ export interface WorkspaceFileVersionRecord { createdAt: Date updatedAt: Date supersededAt: Date | null - secretProvenance: WorkspaceFileSecretProvenanceSnapshot } /** Reads a stored status back, treating anything unrecognized as unknown so a revert fails closed. */ @@ -399,7 +411,7 @@ function toSnapshotStatus(status: string | null): WorkspaceFileSecretProvenanceS return 'unknown' } -function toVersionRecord(row: WorkspaceFileVersionRow): WorkspaceFileVersionRecord { +function toVersionRecord(row: WorkspaceFileVersionSummaryRow): WorkspaceFileVersionRecord { return { fileId: row.fileId, version: row.version, @@ -413,18 +425,13 @@ function toVersionRecord(row: WorkspaceFileVersionRow): WorkspaceFileVersionReco createdAt: row.createdAt, updatedAt: row.updatedAt, supersededAt: row.supersededAt, - secretProvenance: { - status: toSnapshotStatus(row.secretProvenanceStatus), - entries: row.secretProvenanceEntries, - }, } } /** * Version 1 of a file with no history yet — every file before its first content write. Attributed * exactly as {@link recordWorkspaceFileVersionInTx} will materialize it, so the version a reader sees - * keeps its identity once it becomes a row. Its provenance is read live by callers that need it, - * since the sidecar still describes these bytes. + * keeps its identity once it becomes a row. */ function implicitFirstVersion(file: WorkspaceFileVersionSubject): WorkspaceFileVersionRecord { const contentUpdatedAt = file.contentUpdatedAt ?? file.updatedAt @@ -442,7 +449,6 @@ function implicitFirstVersion(file: WorkspaceFileVersionSubject): WorkspaceFileV createdAt: contentUpdatedAt, updatedAt: contentUpdatedAt, supersededAt: null, - secretProvenance: { status: null, entries: [] }, } } @@ -457,7 +463,7 @@ export async function queryWorkspaceFileVersions( ): Promise<{ versions: WorkspaceFileVersionRecord[]; nextKeys: CursorKey[] | null }> { const resume = resumeKeyset(VERSION_KEYSET, options.after, options.sortOrder) const rows = await db - .select() + .select(versionSummaryColumns) .from(workspaceFileVersion) .where(and(eq(workspaceFileVersion.fileId, file.id), resume)) .orderBy(...listOrderBy(keysetColumns(VERSION_KEYSET), options.sortOrder)) @@ -498,21 +504,19 @@ export async function getCurrentWorkspaceFileVersion( } /** - * The version number of the content a file record describes, matched by the record's storage key so - * it stays exact even if a write committed after the record was read. Returns null when the record - * is stale in a way its key cannot answer — a later write replaced those bytes within their version - * and released the key — so the caller re-reads the record rather than guess. + * The current version number of the enclosing query's `workspace_files` row, as a correlated + * subquery so the row and its number come from one statement's snapshot. Content writes record + * their version in the transaction that replaces the bytes, so the newest row describes the current + * content; a file with no rows is on its implicit version 1. Both sides of the correlation + * are table-qualified because Drizzle renders single-table columns bare, which would bind the outer + * `id` to this subquery's own table. */ -export async function getWorkspaceFileVersionNumberForRecord( - file: Pick -): Promise { - const [row] = await db - .select({ version: workspaceFileVersion.version }) - .from(workspaceFileVersion) - .where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.key, file.key))) - .limit(1) - if (row) return row.version - return (await loadWorkspaceFileVersionHead(file.id)) ? null : 1 +export function currentWorkspaceFileVersionNumberSql() { + const versionFileId = sql`${workspaceFileVersion}.${sql.identifier(workspaceFileVersion.fileId.name)}` + const outerFileId = sql`${workspaceFiles}.${sql.identifier(workspaceFiles.id.name)}` + return sql`coalesce((select max(${workspaceFileVersion.version}) from ${workspaceFileVersion} where ${versionFileId} = ${outerFileId}), 1)`.mapWith( + Number + ) } /** One version of a file, or null when it never existed or retention removed it. */ @@ -521,7 +525,7 @@ export async function getWorkspaceFileVersion( version: number ): Promise { const [row] = await db - .select() + .select(versionSummaryColumns) .from(workspaceFileVersion) .where(and(eq(workspaceFileVersion.fileId, file.id), eq(workspaceFileVersion.version, version))) .limit(1) @@ -529,3 +533,35 @@ export async function getWorkspaceFileVersion( if (version !== 1 || (await loadWorkspaceFileVersionHead(file.id))) return null return implicitFirstVersion(file) } + +/** + * The provenance captured with one stored version's bytes, which a revert reinstates; null when the + * row is gone, so a caller never reinstates a classification it did not read. + */ +export async function getWorkspaceFileVersionProvenance( + fileId: string, + version: number +): Promise { + const [row] = await db + .select({ + status: workspaceFileVersion.secretProvenanceStatus, + entries: workspaceFileVersion.secretProvenanceEntries, + }) + .from(workspaceFileVersion) + .where(and(eq(workspaceFileVersion.fileId, fileId), eq(workspaceFileVersion.version, version))) + .limit(1) + if (!row) return null + return { status: toSnapshotStatus(row.status), entries: row.entries } +} + +/** Storage keys of every version of a file, for releasing them with the file row. */ +export async function listWorkspaceFileVersionKeysInTx( + tx: DbTransaction, + fileId: string +): Promise { + const rows = await tx + .select({ key: workspaceFileVersion.key }) + .from(workspaceFileVersion) + .where(eq(workspaceFileVersion.fileId, fileId)) + return rows.map((row) => row.key) +} diff --git a/apps/sim/lib/uploads/core/errors.ts b/apps/sim/lib/uploads/core/errors.ts index aed6e8d2844..d68c3cd7386 100644 --- a/apps/sim/lib/uploads/core/errors.ts +++ b/apps/sim/lib/uploads/core/errors.ts @@ -1,3 +1,5 @@ +import { findCause } from '@sim/utils/errors' + const OBJECT_NOT_FOUND_LABELS = new Set(['NotFound', 'NoSuchKey', 'BlobNotFound', 'ENOENT']) /** @@ -51,10 +53,5 @@ export function isObjectNotFoundError(error: unknown): boolean { * that read an object through a layer which re-wraps storage failures but keeps the original. */ export function hasObjectNotFoundCause(error: unknown): boolean { - let current: unknown = error - for (let depth = 0; current && depth < 5; depth++) { - if (isObjectNotFoundError(current)) return true - current = (current as { cause?: unknown }).cause - } - return false + return findCause(error, (value): value is unknown => isObjectNotFoundError(value)) !== undefined } diff --git a/apps/sim/lib/users/queries.ts b/apps/sim/lib/users/queries.ts index 480f2f01c22..969e9345b0f 100644 --- a/apps/sim/lib/users/queries.ts +++ b/apps/sim/lib/users/queries.ts @@ -103,23 +103,11 @@ export async function getUserEmailById(userId: string): Promise { * never replaced with the raw ID or a placeholder. */ export async function getUserEmailsByIds(userIds: readonly string[]): Promise> { - const uniqueIds = Array.from(new Set(userIds)) - if (uniqueIds.length === 0) return new Map() - if (uniqueIds.length > MAX_USER_EMAIL_BATCH) { - throw new Error(`Cannot resolve more than ${MAX_USER_EMAIL_BATCH} user emails at once`) - } - - const rows = await db - .select({ id: user.id, email: user.email }) - .from(user) - .where(inArray(user.id, uniqueIds)) - - const emailByUserId = new Map(rows.map((row) => [row.id, row.email])) - const missingIds = uniqueIds.filter((id) => !emailByUserId.has(id)) + const emailByUserId = await findUserEmailsByIds(userIds) + const missingIds = Array.from(new Set(userIds)).filter((id) => !emailByUserId.has(id)) if (missingIds.length > 0) { throw new Error(`Unable to resolve email for user IDs: ${missingIds.join(', ')}`) } - return emailByUserId } diff --git a/apps/sim/lib/workspace-files/application/file-versions.test.ts b/apps/sim/lib/workspace-files/application/file-versions.test.ts index d0811b7b3f6..51fc8a3f781 100644 --- a/apps/sim/lib/workspace-files/application/file-versions.test.ts +++ b/apps/sim/lib/workspace-files/application/file-versions.test.ts @@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({ deleteStored: vi.fn(), getVersion: vi.fn(), getCurrentVersion: vi.fn(), + getProvenance: vi.fn(), streamRecord: vi.fn(), recordAudit: vi.fn(), notify: vi.fn(), @@ -48,6 +49,7 @@ vi.mock('@/lib/uploads/contexts/workspace', () => ({ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ getCurrentWorkspaceFileVersion: mocks.getCurrentVersion, getWorkspaceFileVersion: mocks.getVersion, + getWorkspaceFileVersionProvenance: mocks.getProvenance, queryWorkspaceFileVersions: vi.fn(), })) vi.mock('@/lib/workspace-files/application/download-workspace-file', () => ({ @@ -106,7 +108,6 @@ function version(number: number, overrides: Record = {}) { createdAt: new Date('2026-01-02T00:00:00Z'), updatedAt: new Date('2026-01-02T00:00:00Z'), supersededAt: new Date('2026-01-03T00:00:00Z'), - secretProvenance: { status: 'exact', entries: [] }, ...overrides, } } @@ -130,6 +131,7 @@ describe('file version use cases', () => { number === 3 ? current : number === 2 ? version(2) : null ) mocks.fetchBuffer.mockResolvedValue(Buffer.from('old content')) + mocks.getProvenance.mockResolvedValue({ status: 'exact', entries: [] }) mocks.updateContent.mockResolvedValue({ ...file, key: 'new-key', currentVersion: 4 }) mocks.notify.mockResolvedValue(undefined) }) @@ -167,7 +169,6 @@ describe('file version use cases', () => { expect(result).toMatchObject({ reverted: true, revertedFrom: 3, - revertedTo: 2, version: { version: 4, restoredFromVersion: 2 }, }) expect(mocks.recordAudit).toHaveBeenCalledWith( @@ -180,9 +181,22 @@ describe('file version use cases', () => { }), }) ) + expect(mocks.getProvenance).toHaveBeenCalledWith('file-1', 2) expect(mocks.notify).toHaveBeenCalledWith('workspace-1') }) + it('refuses to reinstate provenance it could not read', async () => { + mocks.getProvenance.mockResolvedValueOnce(null) + + await expect( + revertWorkspaceFileVersion.execute({ + principal, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', version: 2 }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(mocks.updateContent).not.toHaveBeenCalled() + }) + it('does nothing, audits nothing, and notifies no one when the version is already current', async () => { const result = await revertWorkspaceFileVersion.execute({ principal, diff --git a/apps/sim/lib/workspace-files/application/file-versions.ts b/apps/sim/lib/workspace-files/application/file-versions.ts index 4a984af0c9e..1c13b904dfc 100644 --- a/apps/sim/lib/workspace-files/application/file-versions.ts +++ b/apps/sim/lib/workspace-files/application/file-versions.ts @@ -17,6 +17,7 @@ import { import { getCurrentWorkspaceFileVersion, getWorkspaceFileVersion, + getWorkspaceFileVersionProvenance, queryWorkspaceFileVersions, type WorkspaceFileVersionRecord, } from '@/lib/uploads/contexts/workspace/workspace-file-versions' @@ -55,13 +56,11 @@ export interface ListWorkspaceFileVersionsInput extends FileVersionTarget { } export interface ListWorkspaceFileVersionsResult { - file: WorkspaceFileRecord versions: WorkspaceFileVersionRecord[] nextKeys: CursorKey[] | null } export interface ReadWorkspaceFileVersionResult { - file: WorkspaceFileRecord version: WorkspaceFileVersionRecord } @@ -88,8 +87,6 @@ export interface RevertWorkspaceFileVersionResult { reverted: boolean /** The version that was current before the revert. */ revertedFrom: number - /** The version whose bytes were requested. */ - revertedTo: number } async function loadActiveFile(context: ActiveWorkspaceFileContext): Promise { @@ -111,15 +108,12 @@ async function loadVersion( * Runs a read of a version's stored object, answering 404 when the object is gone — retention or a * delete can remove a superseded version between loading its row and reading its bytes. */ -async function readVersionObject( - version: WorkspaceFileVersionRecord, - read: () => Promise -): Promise { +async function readVersionObject(version: number, read: () => Promise): Promise { try { return await read() } catch (error) { if (hasObjectNotFoundCause(error)) { - throw new OrchestrationError('not_found', `Version ${version.version} not found`) + throw new OrchestrationError('not_found', `Version ${version} not found`) } throw error } @@ -155,7 +149,7 @@ export const listWorkspaceFileVersions = defineAuthorizedWorkspaceFileUseCase({ limit: input.limit, after: input.after, }) - return { file, versions, nextKeys } + return { versions, nextKeys } }, }) @@ -165,7 +159,7 @@ export const readWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ resolveActiveWorkspaceFileContext(input), async execute({ input, context }): Promise { const file = await loadActiveFile(context) - return { file, version: await loadVersion(file, input.version) } + return { version: await loadVersion(file, input.version) } }, }) @@ -181,15 +175,11 @@ export const readWorkspaceFileVersionText = defineAuthorizedWorkspaceFileUseCase }): Promise { const file = await loadActiveFile(context) const version = await loadVersion(file, input.version) - const result = await readVersionObject(version, () => - extractWorkspaceFileRecordText( - recordAtVersion(file, version), - input, - principal, - request?.signal - ) + const fileAtVersion = recordAtVersion(file, version) + const result = await readVersionObject(version.version, () => + extractWorkspaceFileRecordText(fileAtVersion, input, principal, request?.signal) ) - return { ...result, file, version } + return { ...result, file: fileAtVersion, version } }, }) @@ -200,7 +190,7 @@ export const downloadWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase async execute({ input, context, principal }): Promise { const file = await loadActiveFile(context) const version = await loadVersion(file, input.version) - const result = await readVersionObject(version, () => + const result = await readVersionObject(version.version, () => streamWorkspaceFileRecord(recordAtVersion(file, version), principal) ) return { ...result, file, version } @@ -242,13 +232,7 @@ async function executeRevertWorkspaceFileVersion({ } const target = await loadVersion(file, input.version) if (target.isCurrent) { - return { - file, - version: target, - reverted: false, - revertedFrom: current.version, - revertedTo: target.version, - } + return { file, version: target, reverted: false, revertedFrom: current.version } } if (target.size > MAX_BUFFERED_TRANSFER_BYTES) { throw new OrchestrationError( @@ -257,11 +241,15 @@ async function executeRevertWorkspaceFileVersion({ ) } - const content = await readVersionObject(target, () => - fetchWorkspaceFileBuffer(recordAtVersion(file, target), { - maxBytes: MAX_BUFFERED_TRANSFER_BYTES, - }) - ) + const [content, provenance] = await Promise.all([ + readVersionObject(target.version, () => + fetchWorkspaceFileBuffer(recordAtVersion(file, target), { + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + }) + ), + getWorkspaceFileVersionProvenance(file.id, target.version), + ]) + if (!provenance) throw new OrchestrationError('not_found', `Version ${target.version} not found`) const attribution = resolvePrincipalAttribution(principal, { workspaceBillingOwnerUserId: context.billedAccountUserId, }) @@ -279,7 +267,7 @@ async function executeRevertWorkspaceFileVersion({ restoredFromVersion: target.version, }), expectedUpdatedAt: file.contentUpdatedAt ?? file.updatedAt, - secretProvenancePolicy: { mode: 'reinstate', snapshot: target.secretProvenance }, + secretProvenancePolicy: { mode: 'reinstate', snapshot: provenance }, } ) } catch (error) { @@ -297,12 +285,9 @@ async function executeRevertWorkspaceFileVersion({ }) return { file: updated, - version: - (await getWorkspaceFileVersion(updated, updated.currentVersion)) ?? - (await getCurrentWorkspaceFileVersion(updated)), + version: await loadVersion(updated, updated.currentVersion), reverted: true, revertedFrom: current.version, - revertedTo: target.version, } } @@ -316,17 +301,17 @@ export const revertWorkspaceFileVersion = defineAuthorizedWorkspaceFileUseCase({ resolveContext: ({ input }: { input: RevertWorkspaceFileVersionInput }) => resolveActiveWorkspaceFileContext(input), execute: executeRevertWorkspaceFileVersion, - projectAudit: ({ result }) => + projectAudit: ({ input, result }) => result.reverted ? { action: AuditAction.FILE_REVERTED, resourceType: AuditResourceType.FILE, resourceId: result.file.id, resourceName: result.file.name, - description: `Reverted file "${result.file.name}" to version ${result.revertedTo}`, + description: `Reverted file "${result.file.name}" to version ${input.version}`, metadata: { previousVersion: result.revertedFrom, - restoredVersion: result.revertedTo, + restoredVersion: input.version, newVersion: result.version.version, }, } diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts index 944420a4afa..7f47b7f262a 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.test.ts @@ -8,11 +8,7 @@ const mocks = vi.hoisted(() => ({ getWorkspaceFile: vi.fn(), getShareForResource: vi.fn(), resolvePermission: vi.fn(), - getVersionNumberForRecord: vi.fn(), -})) - -vi.mock('@/lib/uploads/contexts/workspace/workspace-file-versions', () => ({ - getWorkspaceFileVersionNumberForRecord: mocks.getVersionNumberForRecord, + getWorkspaceFileWithCurrentVersion: vi.fn(), })) vi.mock('@sim/platform-authz/workspace', () => ({ @@ -22,6 +18,7 @@ vi.mock('@sim/platform-authz/workspace', () => ({ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ getWorkspaceFile: mocks.getWorkspaceFile, + getWorkspaceFileWithCurrentVersion: mocks.getWorkspaceFileWithCurrentVersion, loadActiveWorkspaceFileContext: mocks.loadContext, })) @@ -177,42 +174,29 @@ describe('readWorkspaceFileMetadataWithVersion', () => { mocks.resolvePermission.mockResolvedValue('admin') }) - it('reports the version of the stored object the returned record describes', async () => { - mocks.getWorkspaceFile.mockResolvedValueOnce(file) - mocks.getVersionNumberForRecord.mockResolvedValueOnce(4) + it('returns the record with the version number read in the same statement', async () => { + const versioned = { ...file, currentVersion: 4 } + mocks.getWorkspaceFileWithCurrentVersion.mockResolvedValueOnce(versioned) await expect( readWorkspaceFileMetadataWithVersion.execute({ principal, - input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, + input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1', includeDeleted: true }, }) - ).resolves.toEqual({ file, share, currentVersion: 4 }) - expect(mocks.getVersionNumberForRecord).toHaveBeenCalledWith(file) - }) - - it('re-reads a record whose bytes a concurrent write already replaced', async () => { - const rewritten = { ...file, key: 'workspace/ws/data-2.csv' } - mocks.getWorkspaceFile.mockResolvedValueOnce(file).mockResolvedValueOnce(rewritten) - mocks.getVersionNumberForRecord.mockResolvedValueOnce(null).mockResolvedValueOnce(5) - - await expect( - readWorkspaceFileMetadataWithVersion.execute({ - principal, - input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, - }) - ).resolves.toEqual({ file: rewritten, share, currentVersion: 5 }) + ).resolves.toEqual({ file: versioned, share }) + expect(mocks.getWorkspaceFileWithCurrentVersion).toHaveBeenCalledWith('workspace-1', 'file-1', { + includeDeleted: true, + }) }) - it('answers a retryable conflict when every read is already stale', async () => { - mocks.getWorkspaceFile.mockResolvedValue(file) - mocks.getVersionNumberForRecord.mockResolvedValue(null) + it('answers not found when the file row is gone', async () => { + mocks.getWorkspaceFileWithCurrentVersion.mockResolvedValueOnce(null) await expect( readWorkspaceFileMetadataWithVersion.execute({ principal, input: { fileId: 'file-1', assertedWorkspaceId: 'workspace-1' }, }) - ).rejects.toMatchObject({ code: 'conflict' }) - expect(mocks.getVersionNumberForRecord).toHaveBeenCalledTimes(3) + ).rejects.toMatchObject({ code: 'not_found' }) }) }) diff --git a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts index 71026e77514..7f1f854fe67 100644 --- a/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts +++ b/apps/sim/lib/workspace-files/application/read-workspace-file-metadata.ts @@ -5,9 +5,9 @@ import { getShareForResource } from '@/lib/public-shares/share-manager' import { type ActiveWorkspaceFileContext, getWorkspaceFile, + getWorkspaceFileWithCurrentVersion, type WorkspaceFileRecord, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { getWorkspaceFileVersionNumberForRecord } from '@/lib/uploads/contexts/workspace/workspace-file-versions' import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case' import { fileOperations } from '@/lib/workspace-files/application/operations' import { resolveActiveWorkspaceFileContext } from '@/lib/workspace-files/application/workspace-file-context' @@ -31,7 +31,7 @@ export interface ReadWorkspaceFileMetadataResult { export interface ReadWorkspaceFileMetadataWithVersionResult extends ReadWorkspaceFileMetadataResult { - currentVersion: number + file: WorkspaceFileRecord & { currentVersion: number } } async function executeReadWorkspaceFileMetadata({ @@ -59,29 +59,22 @@ export const readWorkspaceFileMetadata = defineAuthorizedWorkspaceFileUseCase({ execute: executeReadWorkspaceFileMetadata, }) -/** Reads of a record a concurrent write keeps replacing before the version is reported as a conflict. */ -const CURRENT_VERSION_READ_ATTEMPTS = 3 - /** * The same read plus the current version number, for the public metadata surface. Kept separate so - * the many internal callers of {@link readWorkspaceFileMetadata} pay no extra query. The number is - * resolved from the returned record's own storage key, so it always identifies the content that - * record describes; a record whose bytes a concurrent write already replaced is read again, and a - * file rewritten on every attempt answers a retryable conflict rather than a mismatched version. + * the many internal callers of {@link readWorkspaceFileMetadata} pay nothing for it. */ export const readWorkspaceFileMetadataWithVersion = defineAuthorizedWorkspaceFileUseCase({ operation: fileOperations.readMetadata, resolveContext: ({ input }: { input: ReadWorkspaceFileMetadataInput }) => resolveActiveWorkspaceFileContext(input), - async execute(args): Promise { - for (let attempt = 0; attempt < CURRENT_VERSION_READ_ATTEMPTS; attempt++) { - const result = await executeReadWorkspaceFileMetadata(args) - const currentVersion = await getWorkspaceFileVersionNumberForRecord(result.file) - if (currentVersion !== null) return { ...result, currentVersion } - } - throw new OrchestrationError( - 'conflict', - 'The file changed while it was being read; retry the request' - ) + async execute({ input, context }): Promise { + const [file, share] = await Promise.all([ + getWorkspaceFileWithCurrentVersion(context.workspaceId, context.fileId, { + includeDeleted: input.includeDeleted, + }), + getShareForResource('file', context.fileId), + ]) + if (!file) throw new OrchestrationError('not_found', 'File not found') + return { file, share } }, }) diff --git a/packages/db/migrations/0365_workspace_file_versions.sql b/packages/db/migrations/0365_workspace_file_versions.sql index e37964a79be..e2879246281 100644 --- a/packages/db/migrations/0365_workspace_file_versions.sql +++ b/packages/db/migrations/0365_workspace_file_versions.sql @@ -8,7 +8,6 @@ CREATE TABLE "workspace_file_version" ( "size_bytes" bigint NOT NULL, "content_type" text NOT NULL, "content_hash" text, - "content_updated_at" timestamp NOT NULL, "superseded_at" timestamp, "source" "workspace_file_version_source" NOT NULL, "author_user_ids" text[] DEFAULT '{}'::text[] NOT NULL, diff --git a/packages/db/migrations/meta/0365_snapshot.json b/packages/db/migrations/meta/0365_snapshot.json index 1b8ad3da8ca..35bfe63222f 100644 --- a/packages/db/migrations/meta/0365_snapshot.json +++ b/packages/db/migrations/meta/0365_snapshot.json @@ -26299,12 +26299,6 @@ "primaryKey": false, "notNull": false }, - "content_updated_at": { - "name": "content_updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, "superseded_at": { "name": "superseded_at", "type": "timestamp", diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 542d993db80..04b066d2fdd 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2755,8 +2755,6 @@ export const workspaceFileVersion = pgTable( contentType: text('content_type').notNull(), /** sha256 (hex) of the bytes; NULL for an implicit version materialized without reading them. */ contentHash: text('content_hash'), - /** The file's `contentUpdatedAt` while these bytes were current (millisecond precision). */ - contentUpdatedAt: timestamp('content_updated_at').notNull(), supersededAt: timestamp('superseded_at'), source: workspaceFileVersionSourceEnum('source').notNull(), /** Users who wrote these bytes, in first-contribution order; empty when unattributable. */ diff --git a/packages/sim-cli/src/http/client.test.ts b/packages/sim-cli/src/http/client.test.ts index 25b4c07989a..1b45a65a76a 100644 --- a/packages/sim-cli/src/http/client.test.ts +++ b/packages/sim-cli/src/http/client.test.ts @@ -1104,7 +1104,6 @@ describe('destructive operations are gated', () => { 'restoreTableFolder', 'restoreWorkflow', 'resumeWorkflow', - // Writes the old content as a new version, so what it replaces stays revertible. 'revertFileVersion', 'rollbackWorkflow', 'runRowEnrichment',