From 318cb5e4146f17f71f606a1ce4d74af3ce169bb8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 13:12:56 -0700 Subject: [PATCH 01/24] chore(deps): bump js-yaml to 4.2.0 and nodemailer to 8.0.9 in apps/sim (#5067) --- apps/sim/package.json | 4 ++-- bun.lock | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/sim/package.json b/apps/sim/package.json index 6c41645d9d9..9d8cd3f42d4 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -148,7 +148,7 @@ "isolated-vm": "6.0.2", "jose": "6.0.11", "js-tiktoken": "1.0.21", - "js-yaml": "4.1.1", + "js-yaml": "4.2.0", "json5": "2.2.3", "jszip": "3.10.1", "jwt-decode": "^4.0.0", @@ -165,7 +165,7 @@ "next-mdx-remote": "^6.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", - "nodemailer": "8.0.7", + "nodemailer": "8.0.9", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", diff --git a/bun.lock b/bun.lock index 447ad2121cc..3aeb9c70bb8 100644 --- a/bun.lock +++ b/bun.lock @@ -206,7 +206,7 @@ "isolated-vm": "6.0.2", "jose": "6.0.11", "js-tiktoken": "1.0.21", - "js-yaml": "4.1.1", + "js-yaml": "4.2.0", "json5": "2.2.3", "jszip": "3.10.1", "jwt-decode": "^4.0.0", @@ -223,7 +223,7 @@ "next-mdx-remote": "^6.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", - "nodemailer": "8.0.7", + "nodemailer": "8.0.9", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", @@ -2668,7 +2668,7 @@ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], @@ -3040,7 +3040,7 @@ "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], - "nodemailer": ["nodemailer@8.0.7", "", {}, "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow=="], + "nodemailer": ["nodemailer@8.0.9", "", {}, "sha512-5ofa7BUN8+C+Hckh5V2GjeeOGRQBx0CJQA6KxrvuZfC8iU4/q7sLn8XrtEEhJkjV6HdyIiQs7Bba6bTao8JhkA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -3814,6 +3814,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4130,14 +4132,20 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "fumadocs-core/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "fumadocs-core/shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], "fumadocs-mdx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "fumadocs-mdx/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], "fumadocs-openapi/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "fumadocs-openapi/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "fumadocs-openapi/lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], "fumadocs-openapi/shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], @@ -4176,6 +4184,8 @@ "isomorphic-unfetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "json-schema-to-typescript/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "libmime/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], From 2cf51725c9b077e7f541cebf9c11f51e6368c6ec Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 15 Jun 2026 13:19:43 -0700 Subject: [PATCH 02/24] fix(execute): reject only cross-site session execution (CSRF guard) (#5068) --- .../[id]/execute/route.async.test.ts | 38 +++++++++++++++++++ .../app/api/workflows/[id]/execute/route.ts | 13 +++++++ .../sim/lib/core/security/same-origin.test.ts | 32 ++++++++++++++++ apps/sim/lib/core/security/same-origin.ts | 23 +++++++++++ 4 files changed, 106 insertions(+) create mode 100644 apps/sim/lib/core/security/same-origin.test.ts create mode 100644 apps/sim/lib/core/security/same-origin.ts diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index ebce3426622..17f90047c77 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -194,6 +194,44 @@ describe('workflow execute async route', () => { ) }) + it('rejects cross-site session requests before authorization work', async () => { + const req = createMockRequest( + 'POST', + { input: { hello: 'world' } }, + { + 'Content-Type': 'application/json', + 'Sec-Fetch-Site': 'cross-site', + } + ) + const params = Promise.resolve({ id: 'workflow-1' }) + + const response = await POST(req, { params }) + const body = await response.json() + + expect(response.status).toBe(403) + expect(body.error).toBe('Access denied') + expect(mockAuthorizeWorkflowByWorkspacePermission).not.toHaveBeenCalled() + expect(mockEnqueue).not.toHaveBeenCalled() + }) + + it('allows same-site session requests (multi-subdomain Run, e.g. www.)', async () => { + const req = createMockRequest( + 'POST', + { input: { hello: 'world' } }, + { + 'Content-Type': 'application/json', + 'X-Execution-Mode': 'async', + 'Sec-Fetch-Site': 'same-site', + } + ) + const params = Promise.resolve({ id: 'workflow-1' }) + + const response = await POST(req, { params }) + + expect(response.status).toBe(202) + expect(mockEnqueue).toHaveBeenCalled() + }) + it('rejects oversized request bodies before authorization work', async () => { const req = createMockRequest( 'POST', diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 34868195dc1..0ac748e12e2 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -20,6 +20,7 @@ import { getTimeoutErrorMessage, isTimeoutError, } from '@/lib/core/execution-limits' +import { isCrossSiteSessionRequest } from '@/lib/core/security/same-origin' import { generateRequestId } from '@/lib/core/utils/request' import { SSE_HEADERS } from '@/lib/core/utils/sse' import { @@ -393,6 +394,18 @@ async function handleExecutePost( try { const auth = await checkHybridAuth(req, { requireWorkflowId: false }) + + // CSRF guard: reject session-cookie execution that is provably cross-site + // (a different site driving the user's browser). same-origin and same-site + // are allowed so multi-subdomain deployments (e.g. www. calling + // ) keep working. Scoped to session auth — API-key / public-API / + // internal-JWT callers don't use cookies. Not a defense against a non-browser + // client forging headers; that's covered by the credit/rate-limit gates. + if (auth.success && auth.authType === AuthType.SESSION && isCrossSiteSessionRequest(req)) { + reqLogger.warn('Rejected cross-site session-authenticated execute request') + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + const isMcpBridgeRequest = auth.authType === AuthType.INTERNAL_JWT && req.headers.get(MCP_TOOL_BRIDGE_HEADER) === 'true' const useMcpBridgeAuthenticatedUserAsActor = diff --git a/apps/sim/lib/core/security/same-origin.test.ts b/apps/sim/lib/core/security/same-origin.test.ts new file mode 100644 index 00000000000..6b4c9f4f993 --- /dev/null +++ b/apps/sim/lib/core/security/same-origin.test.ts @@ -0,0 +1,32 @@ +/** + * @vitest-environment node + */ +import type { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { isCrossSiteSessionRequest } from '@/lib/core/security/same-origin' + +function makeRequest(headers: Record): NextRequest { + return { headers: new Headers(headers) } as unknown as NextRequest +} + +describe('isCrossSiteSessionRequest', () => { + it('rejects cross-site requests', () => { + expect(isCrossSiteSessionRequest(makeRequest({ 'sec-fetch-site': 'cross-site' }))).toBe(true) + }) + + it('allows same-origin browser fetches', () => { + expect(isCrossSiteSessionRequest(makeRequest({ 'sec-fetch-site': 'same-origin' }))).toBe(false) + }) + + it('allows same-site fetches (sibling subdomains, e.g. www. -> )', () => { + expect(isCrossSiteSessionRequest(makeRequest({ 'sec-fetch-site': 'same-site' }))).toBe(false) + }) + + it('allows user-initiated requests (Sec-Fetch-Site: none)', () => { + expect(isCrossSiteSessionRequest(makeRequest({ 'sec-fetch-site': 'none' }))).toBe(false) + }) + + it('allows requests with no Sec-Fetch-Site header (older clients)', () => { + expect(isCrossSiteSessionRequest(makeRequest({}))).toBe(false) + }) +}) diff --git a/apps/sim/lib/core/security/same-origin.ts b/apps/sim/lib/core/security/same-origin.ts new file mode 100644 index 00000000000..1fb605ef297 --- /dev/null +++ b/apps/sim/lib/core/security/same-origin.ts @@ -0,0 +1,23 @@ +import type { NextRequest } from 'next/server' + +/** + * Returns true when a request is provably cross-site — a browser fetch driven + * from a different site than our own. Used to reject session-cookie CSRF on + * state-changing routes. + * + * `Sec-Fetch-Site` is browser-set and a forbidden header, so page JavaScript + * cannot forge it. A cross-site browser request (the CSRF threat) always reports + * `cross-site`. We deliberately accept `same-origin`, `same-site`, and `none`: + * the app is served across sibling subdomains (e.g. `www.` calling + * ``), so a legitimate `same-site` fetch must NOT be blocked — rejecting + * it 403s real "Run" requests on those origins. An absent header (older clients) + * is also allowed; the conventional CSRF posture is to reject only a provable + * cross-site request. + * + * This is CSRF protection only. It does not defend against a non-browser client + * that forges headers directly (no header check can); that surface is covered by + * the credit and execution rate-limit gates. + */ +export function isCrossSiteSessionRequest(req: NextRequest): boolean { + return req.headers.get('sec-fetch-site') === 'cross-site' +} From 02022e9ab2b3d3b263454874f3b8a63e5624d934 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 13:59:47 -0700 Subject: [PATCH 03/24] fix(providers): pin Azure OpenAI/Anthropic endpoints to validated IP (#5060) * fix(providers): pin Azure OpenAI/Anthropic endpoints to validated IP (TOCTOU SSRF) * fix(providers): fail closed when Azure endpoint validates without a pinnable IP * refactor(security): drop pinned-fetch agent pool, keep per-call dispatcher * refactor(security): make createPinnedFetch the single pinned-fetch source * refactor(security): drop pinned-fetch agent pool; keep per-call dispatcher --- .../core/security/input-validation.server.ts | 35 ++++ .../core/security/pinned-fetch.server.test.ts | 126 ++++++++++++ apps/sim/lib/mcp/client.ts | 4 +- apps/sim/lib/mcp/oauth/probe.test.ts | 36 ++-- apps/sim/lib/mcp/oauth/probe.ts | 5 +- apps/sim/lib/mcp/oauth/revoke.test.ts | 30 +-- apps/sim/lib/mcp/pinned-fetch.test.ts | 157 ++------------- apps/sim/lib/mcp/pinned-fetch.ts | 58 +----- .../providers/azure-anthropic/index.test.ts | 120 +++++++++++ apps/sim/providers/azure-anthropic/index.ts | 8 +- apps/sim/providers/azure-openai/index.test.ts | 190 ++++++++++++++++++ apps/sim/providers/azure-openai/index.ts | 16 +- apps/sim/providers/openai/core.ts | 13 +- 13 files changed, 556 insertions(+), 242 deletions(-) create mode 100644 apps/sim/lib/core/security/pinned-fetch.server.test.ts create mode 100644 apps/sim/providers/azure-anthropic/index.test.ts create mode 100644 apps/sim/providers/azure-openai/index.test.ts diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index cdf5f28a9a1..81fbfe75e5a 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -5,6 +5,7 @@ import type { LookupFunction } from 'net' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' +import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici' import { isHosted } from '@/lib/core/config/feature-flags' import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation' import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' @@ -400,6 +401,40 @@ export function createPinnedLookup(resolvedIP: string): LookupFunction { } } +/** + * Builds a standard `fetch`-compatible function that pins every outbound + * connection to `resolvedIP`, preventing DNS-rebinding (TOCTOU) between URL + * validation and connection. The original hostname is preserved for TLS SNI and + * the `Host` header so it still matches the certificate. This is the single + * source of truth for pinned outbound fetches — both the LLM providers and the + * MCP transport consume it. + * + * Pass the returned function as the `fetch` option to the OpenAI/Anthropic SDKs + * (or call it directly) after validating the URL with {@link validateUrlWithDNS} + * and capturing `resolvedIP`. Because the pinned lookup always returns + * `resolvedIP` regardless of hostname, any redirect the server returns also + * connects to the validated IP — an attacker cannot rebind a redirect target to + * an internal address. + * + * The `Agent` is captured for the lifetime of the returned function, so repeated + * calls (e.g. a provider tool loop) reuse its keep-alive connections. + */ +export function createPinnedFetch(resolvedIP: string): typeof fetch { + const dispatcher = new Agent({ connect: { lookup: createPinnedLookup(resolvedIP) } }) + + const pinned = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + // double-cast-allowed: DOM RequestInfo/URL and undici fetch input types differ but are structurally compatible at runtime (Node's global fetch IS undici) + const undiciInput = input as unknown as Parameters[0] + // double-cast-allowed: DOM RequestInit and undici RequestInit are structurally compatible at runtime but the TS types differ + const undiciInit: UndiciRequestInit = { ...(init as unknown as UndiciRequestInit), dispatcher } + const response = await undiciFetch(undiciInput, undiciInit) + // double-cast-allowed: undici Response and DOM Response are structurally compatible at runtime + return response as unknown as Response + } + + return pinned +} + /** * Performs a fetch with IP pinning to prevent DNS rebinding attacks. * Uses the pre-resolved IP address while preserving the original hostname for TLS SNI. diff --git a/apps/sim/lib/core/security/pinned-fetch.server.test.ts b/apps/sim/lib/core/security/pinned-fetch.server.test.ts new file mode 100644 index 00000000000..d63bad257ee --- /dev/null +++ b/apps/sim/lib/core/security/pinned-fetch.server.test.ts @@ -0,0 +1,126 @@ +/** + * @vitest-environment node + */ +import { featureFlagsMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockAgent, mockUndiciFetch, capturedAgentOptions, agentCloses } = vi.hoisted(() => { + const capturedAgentOptions: unknown[] = [] + const agentCloses: unknown[] = [] + class MockAgent { + constructor(options: unknown) { + capturedAgentOptions.push(options) + } + close() { + agentCloses.push(this) + return Promise.resolve() + } + } + return { + mockAgent: MockAgent, + mockUndiciFetch: vi.fn(), + capturedAgentOptions, + agentCloses, + } +}) + +vi.mock('undici', () => ({ Agent: mockAgent, fetch: mockUndiciFetch })) +vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) + +import { createPinnedFetch } from '@/lib/core/security/input-validation.server' + +type LookupCallback = (err: Error | null, address: string, family: number) => void +type PinnedLookup = (hostname: string, options: { all?: boolean }, callback: LookupCallback) => void + +describe('createPinnedFetch', () => { + beforeEach(() => { + vi.clearAllMocks() + capturedAgentOptions.length = 0 + agentCloses.length = 0 + mockUndiciFetch.mockResolvedValue(new Response('ok')) + }) + + it('builds an undici Agent whose pinned lookup always resolves to the validated IP', async () => { + createPinnedFetch('203.0.113.10') + + expect(capturedAgentOptions).toHaveLength(1) + const { connect } = capturedAgentOptions[0] as { connect: { lookup: PinnedLookup } } + expect(typeof connect.lookup).toBe('function') + + const resolved = await new Promise<{ address: string; family: number }>((resolve) => { + connect.lookup('rebind.attacker.tld', {}, (_err, address, family) => + resolve({ address, family }) + ) + }) + expect(resolved).toEqual({ address: '203.0.113.10', family: 4 }) + }) + + it('uses IPv6 family when the validated IP is IPv6', async () => { + createPinnedFetch('2606:4700:4700::1111') + const { connect } = capturedAgentOptions[0] as { connect: { lookup: PinnedLookup } } + const resolved = await new Promise<{ address: string; family: number }>((resolve) => { + connect.lookup('example.com', {}, (_err, address, family) => resolve({ address, family })) + }) + expect(resolved).toEqual({ address: '2606:4700:4700::1111', family: 6 }) + }) + + it('forwards the pinned dispatcher on every call while preserving init options', async () => { + const pinned = createPinnedFetch('203.0.113.10') + const controller = new AbortController() + + await pinned('https://myresource.openai.azure.com/openai/v1/responses', { + method: 'POST', + headers: { 'api-key': 'secret' }, + body: '{}', + signal: controller.signal, + }) + + expect(mockUndiciFetch).toHaveBeenCalledTimes(1) + const [url, init] = mockUndiciFetch.mock.calls[0] + expect(url).toBe('https://myresource.openai.azure.com/openai/v1/responses') + const typedInit = init as RequestInit & { dispatcher?: unknown } + expect(typedInit.dispatcher).toBeInstanceOf(mockAgent) + expect(typedInit.method).toBe('POST') + expect(typedInit.headers).toEqual({ 'api-key': 'secret' }) + expect(typedInit.body).toBe('{}') + expect(typedInit.signal).toBe(controller.signal) + }) + + it('handles an undefined init by still attaching the dispatcher', async () => { + const pinned = createPinnedFetch('203.0.113.10') + await pinned('https://example.com') + const init = mockUndiciFetch.mock.calls[0][1] as { dispatcher?: unknown } + expect(init.dispatcher).toBeInstanceOf(mockAgent) + }) + + it('reuses one captured dispatcher across all calls of a single instance', async () => { + const pinned = createPinnedFetch('203.0.113.10') + await pinned('https://example.com/a') + await pinned('https://example.com/b') + + expect(capturedAgentOptions).toHaveLength(1) + const d1 = (mockUndiciFetch.mock.calls[0][1] as { dispatcher: unknown }).dispatcher + const d2 = (mockUndiciFetch.mock.calls[1][1] as { dispatcher: unknown }).dispatcher + expect(d1).toBe(d2) + }) + + it('creates an independent dispatcher per instance', async () => { + const a = createPinnedFetch('203.0.113.10') + const b = createPinnedFetch('203.0.113.10') + await a('https://example.com/a') + await b('https://example.com/b') + + expect(capturedAgentOptions).toHaveLength(2) + const d1 = (mockUndiciFetch.mock.calls[0][1] as { dispatcher: unknown }).dispatcher + const d2 = (mockUndiciFetch.mock.calls[1][1] as { dispatcher: unknown }).dispatcher + expect(d1).not.toBe(d2) + }) + + it('returns the response produced by undici fetch', async () => { + mockUndiciFetch.mockResolvedValueOnce(new Response('pong', { status: 201 })) + const pinned = createPinnedFetch('203.0.113.10') + const response = await pinned('https://example.com') + expect(response.status).toBe(201) + expect(await response.text()).toBe('pong') + }) +}) diff --git a/apps/sim/lib/mcp/client.ts b/apps/sim/lib/mcp/client.ts index bef88182c9e..569320ef882 100644 --- a/apps/sim/lib/mcp/client.ts +++ b/apps/sim/lib/mcp/client.ts @@ -11,8 +11,8 @@ import { import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import { createPinnedFetch } from '@/lib/core/security/input-validation.server' import { McpOauthRedirectRequired } from '@/lib/mcp/oauth' -import { createMcpPinnedFetch } from '@/lib/mcp/pinned-fetch' import { type McpClientOptions, McpConnectionError, @@ -70,7 +70,7 @@ export class McpClient { this.transport = new StreamableHTTPClientTransport(new URL(this.config.url), { authProvider: useOauth ? this.authProvider : undefined, requestInit: { headers: this.config.headers }, - ...(resolvedIP ? { fetch: createMcpPinnedFetch(resolvedIP) } : {}), + ...(resolvedIP ? { fetch: createPinnedFetch(resolvedIP) } : {}), }) this.client = new Client( diff --git a/apps/sim/lib/mcp/oauth/probe.test.ts b/apps/sim/lib/mcp/oauth/probe.test.ts index 34e7d6199e4..d691f1178c7 100644 --- a/apps/sim/lib/mcp/oauth/probe.test.ts +++ b/apps/sim/lib/mcp/oauth/probe.test.ts @@ -3,24 +3,22 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { - mockCreateMcpPinnedFetch, - mockCreateSsrfGuardedMcpFetch, - mockPinnedFetch, - mockGuardedFetch, -} = vi.hoisted(() => { - const mockPinnedFetch = vi.fn() - const mockGuardedFetch = vi.fn() - return { - mockPinnedFetch, - mockGuardedFetch, - mockCreateMcpPinnedFetch: vi.fn(() => mockPinnedFetch), - mockCreateSsrfGuardedMcpFetch: vi.fn(() => mockGuardedFetch), - } -}) +const { mockCreatePinnedFetch, mockCreateSsrfGuardedMcpFetch, mockPinnedFetch, mockGuardedFetch } = + vi.hoisted(() => { + const mockPinnedFetch = vi.fn() + const mockGuardedFetch = vi.fn() + return { + mockPinnedFetch, + mockGuardedFetch, + mockCreatePinnedFetch: vi.fn(() => mockPinnedFetch), + mockCreateSsrfGuardedMcpFetch: vi.fn(() => mockGuardedFetch), + } + }) +vi.mock('@/lib/core/security/input-validation.server', () => ({ + createPinnedFetch: mockCreatePinnedFetch, +})) vi.mock('@/lib/mcp/pinned-fetch', () => ({ - createMcpPinnedFetch: mockCreateMcpPinnedFetch, createSsrfGuardedMcpFetch: mockCreateSsrfGuardedMcpFetch, })) @@ -50,7 +48,7 @@ describe('detectMcpAuthType — connection pinning (SSRF / DNS-rebinding)', () = const authType = await detectMcpAuthType('https://rebind.example.com/mcp', '203.0.113.10') expect(authType).toBe('none') - expect(mockCreateMcpPinnedFetch).toHaveBeenCalledWith('203.0.113.10') + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') expect(mockCreateSsrfGuardedMcpFetch).not.toHaveBeenCalled() expect(mockPinnedFetch).toHaveBeenCalledTimes(1) // The unpinned global fetch must never be used — that was the SSRF sink. @@ -64,7 +62,7 @@ describe('detectMcpAuthType — connection pinning (SSRF / DNS-rebinding)', () = expect(authType).toBe('none') expect(mockCreateSsrfGuardedMcpFetch).toHaveBeenCalledTimes(1) - expect(mockCreateMcpPinnedFetch).not.toHaveBeenCalled() + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() expect(mockGuardedFetch).toHaveBeenCalledTimes(1) expect(globalFetchSpy).not.toHaveBeenCalled() }) @@ -90,7 +88,7 @@ describe('detectMcpAuthType — connection pinning (SSRF / DNS-rebinding)', () = const authType = await detectMcpAuthType('http://example.com/mcp', '203.0.113.10') expect(authType).toBe('headers') - expect(mockCreateMcpPinnedFetch).not.toHaveBeenCalled() + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() expect(mockCreateSsrfGuardedMcpFetch).not.toHaveBeenCalled() expect(globalFetchSpy).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/mcp/oauth/probe.ts b/apps/sim/lib/mcp/oauth/probe.ts index 887ba8ce971..4343f27870a 100644 --- a/apps/sim/lib/mcp/oauth/probe.ts +++ b/apps/sim/lib/mcp/oauth/probe.ts @@ -1,8 +1,9 @@ import { extractWWWAuthenticateParams } from '@modelcontextprotocol/sdk/client/auth.js' import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js' import { createLogger } from '@sim/logger' +import { createPinnedFetch } from '@/lib/core/security/input-validation.server' import { isLoopbackHostname } from '@/lib/core/utils/urls' -import { createMcpPinnedFetch, createSsrfGuardedMcpFetch } from '@/lib/mcp/pinned-fetch' +import { createSsrfGuardedMcpFetch } from '@/lib/mcp/pinned-fetch' import type { McpAuthType } from '@/lib/mcp/types' const logger = createLogger('McpOauthProbe') @@ -33,7 +34,7 @@ export async function detectMcpAuthType( } const probeFetch: FetchLike = resolvedIP - ? createMcpPinnedFetch(resolvedIP) + ? createPinnedFetch(resolvedIP) : createSsrfGuardedMcpFetch() const controller = new AbortController() diff --git a/apps/sim/lib/mcp/oauth/revoke.test.ts b/apps/sim/lib/mcp/oauth/revoke.test.ts index d8f6342568b..ccd0caba98b 100644 --- a/apps/sim/lib/mcp/oauth/revoke.test.ts +++ b/apps/sim/lib/mcp/oauth/revoke.test.ts @@ -15,33 +15,23 @@ const PUBLIC_SERVER_URL = 'https://mcp.attacker.com' const PUBLIC_SERVER_IP = '203.0.113.10' const { - MockAgent, mockUndiciFetch, mockValidateMcpServerSsrf, mockDiscoverOAuthServerInfo, mockLoadOauthRow, mockDecryptSecret, mockDbSelect, -} = vi.hoisted(() => { - class MockAgent { - close() { - return Promise.resolve() - } - } - return { - MockAgent, - mockUndiciFetch: vi.fn(), - mockValidateMcpServerSsrf: vi.fn(), - mockDiscoverOAuthServerInfo: vi.fn(), - mockLoadOauthRow: vi.fn(), - mockDecryptSecret: vi.fn(), - mockDbSelect: vi.fn(), - } -}) +} = vi.hoisted(() => ({ + mockUndiciFetch: vi.fn(), + mockValidateMcpServerSsrf: vi.fn(), + mockDiscoverOAuthServerInfo: vi.fn(), + mockLoadOauthRow: vi.fn(), + mockDecryptSecret: vi.fn(), + mockDbSelect: vi.fn(), +})) -vi.mock('undici', () => ({ Agent: MockAgent, fetch: mockUndiciFetch })) vi.mock('@/lib/core/security/input-validation.server', () => ({ - createPinnedLookup: vi.fn(() => 'pinned-lookup-fn'), + createPinnedFetch: vi.fn(() => mockUndiciFetch), })) vi.mock('@/lib/mcp/domain-check', () => ({ validateMcpServerSsrf: mockValidateMcpServerSsrf, @@ -59,7 +49,6 @@ vi.mock('@sim/db', () => ({ db: { select: mockDbSelect }, })) -import { __resetPinnedAgentsForTests } from '@/lib/mcp/pinned-fetch' import { revokeMcpOauthTokens } from './revoke' function wireServerRow(row: Record) { @@ -74,7 +63,6 @@ function wireServerRow(row: Record) { describe('revokeMcpOauthTokens — SSRF guard', () => { beforeEach(() => { vi.clearAllMocks() - __resetPinnedAgentsForTests() mockLoadOauthRow.mockResolvedValue({ tokens: { access_token: 'access-secret', refresh_token: 'refresh-secret' }, diff --git a/apps/sim/lib/mcp/pinned-fetch.test.ts b/apps/sim/lib/mcp/pinned-fetch.test.ts index 9f6b5919bf2..64354ee708b 100644 --- a/apps/sim/lib/mcp/pinned-fetch.test.ts +++ b/apps/sim/lib/mcp/pinned-fetch.test.ts @@ -3,147 +3,26 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockAgent, mockCreatePinnedLookup, mockUndiciFetch, capturedAgentOptions, agentCloses } = - vi.hoisted(() => { - const capturedAgentOptions: unknown[] = [] - const agentCloses: unknown[] = [] - class MockAgent { - constructor(options: unknown) { - capturedAgentOptions.push(options) - } - close() { - agentCloses.push(this) - return Promise.resolve() - } - } - return { - mockAgent: MockAgent, - mockCreatePinnedLookup: vi.fn(), - mockUndiciFetch: vi.fn(), - capturedAgentOptions, - agentCloses, - } - }) - -const { mockValidateMcpServerSsrf } = vi.hoisted(() => ({ +const { mockCreatePinnedFetch, mockValidateMcpServerSsrf, sentinelFetch } = vi.hoisted(() => ({ + mockCreatePinnedFetch: vi.fn(), mockValidateMcpServerSsrf: vi.fn(), + sentinelFetch: vi.fn(), })) -vi.mock('undici', () => ({ Agent: mockAgent, fetch: mockUndiciFetch })) vi.mock('@/lib/core/security/input-validation.server', () => ({ - createPinnedLookup: mockCreatePinnedLookup, + createPinnedFetch: mockCreatePinnedFetch, })) vi.mock('@/lib/mcp/domain-check', () => ({ validateMcpServerSsrf: mockValidateMcpServerSsrf, })) -import { - __resetPinnedAgentsForTests, - createMcpPinnedFetch, - createSsrfGuardedMcpFetch, -} from '@/lib/mcp/pinned-fetch' - -describe('createMcpPinnedFetch', () => { - beforeEach(() => { - vi.clearAllMocks() - capturedAgentOptions.length = 0 - agentCloses.length = 0 - __resetPinnedAgentsForTests() - mockCreatePinnedLookup.mockReturnValue('pinned-lookup-fn') - mockUndiciFetch.mockResolvedValue(new Response('ok')) - }) - - it('builds an undici Agent with the pinned lookup for the resolved IP', () => { - createMcpPinnedFetch('203.0.113.10') - expect(mockCreatePinnedLookup).toHaveBeenCalledWith('203.0.113.10') - expect(capturedAgentOptions).toHaveLength(1) - expect(capturedAgentOptions[0]).toEqual({ connect: { lookup: 'pinned-lookup-fn' } }) - }) - - it('forwards the dispatcher on every fetch call', async () => { - const fetchLike = createMcpPinnedFetch('203.0.113.10') - await fetchLike('https://example.com/mcp', { method: 'POST' }) - expect(mockUndiciFetch).toHaveBeenCalledTimes(1) - const [url, init] = mockUndiciFetch.mock.calls[0] - expect(url).toBe('https://example.com/mcp') - expect((init as { dispatcher?: unknown }).dispatcher).toBeInstanceOf(mockAgent) - expect((init as { method?: string }).method).toBe('POST') - }) - - it('preserves caller-provided init options (headers, signal)', async () => { - const fetchLike = createMcpPinnedFetch('203.0.113.10') - const controller = new AbortController() - await fetchLike('https://example.com/mcp', { - method: 'GET', - headers: { 'x-test': '1' }, - signal: controller.signal, - }) - const init = mockUndiciFetch.mock.calls[0][1] as RequestInit & { dispatcher?: unknown } - expect(init.headers).toEqual({ 'x-test': '1' }) - expect(init.signal).toBe(controller.signal) - expect(init.dispatcher).toBeInstanceOf(mockAgent) - }) - - it('handles undefined init gracefully', async () => { - const fetchLike = createMcpPinnedFetch('203.0.113.10') - await fetchLike('https://example.com/mcp') - const init = mockUndiciFetch.mock.calls[0][1] as { dispatcher?: unknown } - expect(init.dispatcher).toBeInstanceOf(mockAgent) - }) - - it('reuses the same dispatcher across calls within a fetch instance', async () => { - const fetchLike = createMcpPinnedFetch('203.0.113.10') - await fetchLike('https://example.com/a') - await fetchLike('https://example.com/b') - expect(capturedAgentOptions).toHaveLength(1) - const d1 = (mockUndiciFetch.mock.calls[0][1] as { dispatcher: unknown }).dispatcher - const d2 = (mockUndiciFetch.mock.calls[1][1] as { dispatcher: unknown }).dispatcher - expect(d1).toBe(d2) - }) - - it('pools agents by resolvedIP across createMcpPinnedFetch calls', async () => { - const a = createMcpPinnedFetch('203.0.113.10') - const b = createMcpPinnedFetch('203.0.113.10') - await a('https://example.com/a') - await b('https://example.com/b') - expect(capturedAgentOptions).toHaveLength(1) - const d1 = (mockUndiciFetch.mock.calls[0][1] as { dispatcher: unknown }).dispatcher - const d2 = (mockUndiciFetch.mock.calls[1][1] as { dispatcher: unknown }).dispatcher - expect(d1).toBe(d2) - }) - - it('creates separate agents for different resolved IPs', async () => { - const a = createMcpPinnedFetch('203.0.113.10') - const b = createMcpPinnedFetch('198.51.100.20') - await a('https://example.com/a') - await b('https://example.com/b') - expect(capturedAgentOptions).toHaveLength(2) - const d1 = (mockUndiciFetch.mock.calls[0][1] as { dispatcher: unknown }).dispatcher - const d2 = (mockUndiciFetch.mock.calls[1][1] as { dispatcher: unknown }).dispatcher - expect(d1).not.toBe(d2) - }) - - it('does not close evicted agents — captured closures keep working', async () => { - // Build an early closure whose agent will get evicted by later IPs. - const earlyClient = createMcpPinnedFetch('10.0.0.1') - // Fill the cache past its 64-entry limit so the early entry is evicted. - for (let i = 0; i < 64; i++) createMcpPinnedFetch(`10.1.${Math.floor(i / 256)}.${i % 256}`) - - // Eviction must NOT have closed any agents. - expect(agentCloses).toHaveLength(0) - // The early closure's captured dispatcher is still callable. - await earlyClient('https://example.com/still-works') - expect(mockUndiciFetch).toHaveBeenCalledTimes(1) - }) -}) +import { createSsrfGuardedMcpFetch } from '@/lib/mcp/pinned-fetch' describe('createSsrfGuardedMcpFetch', () => { beforeEach(() => { vi.clearAllMocks() - capturedAgentOptions.length = 0 - __resetPinnedAgentsForTests() - mockCreatePinnedLookup.mockReturnValue('pinned-lookup-fn') - mockUndiciFetch.mockResolvedValue(new Response('ok')) + mockCreatePinnedFetch.mockReturnValue(sentinelFetch) + sentinelFetch.mockResolvedValue(new Response('ok')) }) it('validates each request URL and pins to the resolved IP', async () => { @@ -152,11 +31,10 @@ describe('createSsrfGuardedMcpFetch', () => { await fetchLike('https://attacker.example/revoke', { method: 'POST' }) expect(mockValidateMcpServerSsrf).toHaveBeenCalledWith('https://attacker.example/revoke') - expect(mockUndiciFetch).toHaveBeenCalledTimes(1) - const [url, init] = mockUndiciFetch.mock.calls[0] - expect(url).toBe('https://attacker.example/revoke') - expect((init as { dispatcher?: unknown }).dispatcher).toBeInstanceOf(mockAgent) - expect((init as { method?: string }).method).toBe('POST') + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') + expect(sentinelFetch).toHaveBeenCalledWith('https://attacker.example/revoke', { + method: 'POST', + }) }) it('rejects URLs that resolve to blocked IPs without issuing the request', async () => { @@ -166,7 +44,8 @@ describe('createSsrfGuardedMcpFetch', () => { await expect( fetchLike('http://169.254.169.254/latest/meta-data/', { method: 'POST' }) ).rejects.toThrow('blocked') - expect(mockUndiciFetch).not.toHaveBeenCalled() + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(sentinelFetch).not.toHaveBeenCalled() }) it('accepts URL objects and validates their href', async () => { @@ -175,6 +54,14 @@ describe('createSsrfGuardedMcpFetch', () => { await fetchLike(new URL('https://attacker.example/discover')) expect(mockValidateMcpServerSsrf).toHaveBeenCalledWith('https://attacker.example/discover') - expect(mockUndiciFetch).toHaveBeenCalledTimes(1) + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') + }) + + it('falls back to global fetch when validation returns no IP', async () => { + mockValidateMcpServerSsrf.mockResolvedValue(null) + const fetchLike = createSsrfGuardedMcpFetch() + await fetchLike('https://allowed.internal/mcp') + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/mcp/pinned-fetch.ts b/apps/sim/lib/mcp/pinned-fetch.ts index f395d6b03c5..3184a0da7a9 100644 --- a/apps/sim/lib/mcp/pinned-fetch.ts +++ b/apps/sim/lib/mcp/pinned-fetch.ts @@ -1,61 +1,7 @@ import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js' -import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici' -import { createPinnedLookup } from '@/lib/core/security/input-validation.server' +import { createPinnedFetch } from '@/lib/core/security/input-validation.server' import { validateMcpServerSsrf } from '@/lib/mcp/domain-check' -/** - * Pins outbound HTTP connections to a pre-resolved IP to prevent DNS-rebinding - * between URL validation and connection. Hostname is preserved so TLS SNI and - * the Host header still match the certificate. - * - * Agents are pooled by `resolvedIP` so back-to-back calls to the same server - * reuse the same keep-alive connection pool instead of opening a fresh TCP + - * TLS connection per McpClient instance. - */ -const MAX_POOLED_AGENTS = 64 -const pinnedAgents = new Map() - -function getPinnedAgent(resolvedIP: string): Agent { - const existing = pinnedAgents.get(resolvedIP) - if (existing) { - // LRU touch — re-insert to mark as most recently used. - pinnedAgents.delete(resolvedIP) - pinnedAgents.set(resolvedIP, existing) - return existing - } - if (pinnedAgents.size >= MAX_POOLED_AGENTS) { - // Drop the oldest entry WITHOUT closing it — existing `createMcpPinnedFetch` - // closures may still hold a reference and have in-flight requests. The - // dispatcher is GC'd (and its sockets cleaned up) when the last closure - // releases it; undici closes idle keep-alive connections after its own - // timeout (default 4s). - const oldestKey = pinnedAgents.keys().next().value - if (oldestKey !== undefined) pinnedAgents.delete(oldestKey) - } - const agent = new Agent({ connect: { lookup: createPinnedLookup(resolvedIP) } }) - pinnedAgents.set(resolvedIP, agent) - return agent -} - -export function __resetPinnedAgentsForTests(): void { - pinnedAgents.clear() -} - -export function createMcpPinnedFetch(resolvedIP: string): FetchLike { - const dispatcher = getPinnedAgent(resolvedIP) - - return (async (url, init) => { - const undiciInit: UndiciRequestInit = { - // double-cast-allowed: DOM RequestInit and undici RequestInit are structurally compatible at runtime (Node's global fetch IS undici) but the TS types differ - ...(init as unknown as UndiciRequestInit), - dispatcher, - } - const response = await undiciFetch(url as string | URL, undiciInit) - // double-cast-allowed: undici Response and DOM Response are structurally compatible at runtime; bridging the types is required to satisfy the FetchLike contract - return response as unknown as Response - }) satisfies FetchLike -} - /** * Builds a `FetchLike` that validates every outbound request URL against the * MCP SSRF policy before issuing it, then pins the connection to the resolved @@ -79,7 +25,7 @@ export function createSsrfGuardedMcpFetch(): FetchLike { return (async (url, init) => { const target = typeof url === 'string' ? url : url.href const resolvedIP = await validateMcpServerSsrf(target) - const pinnedFetch: FetchLike = resolvedIP ? createMcpPinnedFetch(resolvedIP) : globalThis.fetch + const pinnedFetch: FetchLike = resolvedIP ? createPinnedFetch(resolvedIP) : globalThis.fetch return pinnedFetch(url, init) }) satisfies FetchLike } diff --git a/apps/sim/providers/azure-anthropic/index.test.ts b/apps/sim/providers/azure-anthropic/index.test.ts new file mode 100644 index 00000000000..b5254f9eaf8 --- /dev/null +++ b/apps/sim/providers/azure-anthropic/index.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProviderRequest } from '@/providers/types' + +const { + mockAnthropic, + anthropicArgs, + mockValidate, + mockCreatePinnedFetch, + mockExecuteAnthropic, + sentinelFetch, + envState, +} = vi.hoisted(() => { + const anthropicArgs: Array> = [] + const sentinelFetch = vi.fn() + class MockAnthropic { + constructor(opts: Record) { + anthropicArgs.push(opts) + } + } + return { + mockAnthropic: MockAnthropic, + anthropicArgs, + mockValidate: vi.fn(), + mockCreatePinnedFetch: vi.fn(() => sentinelFetch), + mockExecuteAnthropic: vi.fn(), + sentinelFetch, + envState: { + AZURE_ANTHROPIC_ENDPOINT: undefined as string | undefined, + AZURE_ANTHROPIC_API_VERSION: undefined as string | undefined, + }, + } +}) + +vi.mock('@anthropic-ai/sdk', () => ({ default: mockAnthropic })) +vi.mock('@/lib/core/config/env', () => ({ env: envState })) +vi.mock('@/lib/core/security/input-validation.server', () => ({ + validateUrlWithDNS: mockValidate, + createPinnedFetch: mockCreatePinnedFetch, +})) +vi.mock('@/providers/anthropic/core', () => ({ + executeAnthropicProviderRequest: mockExecuteAnthropic, +})) +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => []), + getProviderDefaultModel: vi.fn(() => 'azure-anthropic/claude'), +})) + +import { azureAnthropicProvider } from '@/providers/azure-anthropic/index' + +function request(overrides: Partial): ProviderRequest { + return { model: 'azure-anthropic/claude-3-5-sonnet', apiKey: 'k', messages: [], ...overrides } +} + +/** Invokes the createClient factory handed to the Anthropic core and returns the SDK options it built. */ +function buildClientOptions(): Record { + const config = mockExecuteAnthropic.mock.calls[0][1] + config.createClient('k', false) + return anthropicArgs[0] +} + +describe('azureAnthropicProvider — SSRF pinning', () => { + beforeEach(() => { + vi.clearAllMocks() + anthropicArgs.length = 0 + envState.AZURE_ANTHROPIC_ENDPOINT = undefined + envState.AZURE_ANTHROPIC_API_VERSION = undefined + mockExecuteAnthropic.mockResolvedValue({ content: 'ok' }) + }) + + it('validates and pins the connection to the resolved IP for a user-supplied endpoint', async () => { + mockValidate.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + + await azureAnthropicProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + + expect(mockValidate).toHaveBeenCalledWith('https://rebind.attacker.tld', 'azureEndpoint') + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') + expect(buildClientOptions()).toMatchObject({ fetch: sentinelFetch }) + }) + + it('does not pin when the endpoint comes from trusted server env', async () => { + envState.AZURE_ANTHROPIC_ENDPOINT = 'https://trusted.services.ai.azure.com' + + await azureAnthropicProvider.executeRequest(request({ azureEndpoint: undefined })) + + expect(mockValidate).not.toHaveBeenCalled() + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(buildClientOptions()).not.toHaveProperty('fetch') + }) + + it('throws and never builds a client when validation blocks the endpoint', async () => { + mockValidate.mockResolvedValue({ isValid: false, error: 'resolves to a blocked IP address' }) + + await expect( + azureAnthropicProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + ).rejects.toThrow('Invalid Azure Anthropic endpoint') + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(mockExecuteAnthropic).not.toHaveBeenCalled() + }) + + it('fails closed when validation passes but yields no resolvable IP to pin', async () => { + mockValidate.mockResolvedValue({ isValid: true }) + + await expect( + azureAnthropicProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + ).rejects.toThrow('could not resolve a pinnable IP address') + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(mockExecuteAnthropic).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts index 999dc0938f8..39980d77c2e 100644 --- a/apps/sim/providers/azure-anthropic/index.ts +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -1,7 +1,7 @@ import Anthropic from '@anthropic-ai/sdk' import { createLogger } from '@sim/logger' import { env } from '@/lib/core/config/env' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { createPinnedFetch, validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import type { StreamingExecution } from '@/executor/types' import { executeAnthropicProviderRequest } from '@/providers/anthropic/core' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' @@ -28,6 +28,7 @@ export const azureAnthropicProvider: ProviderConfig = { ) } + let pinnedFetch: typeof fetch | undefined if (userProvidedEndpoint) { const validation = await validateUrlWithDNS(userProvidedEndpoint, 'azureEndpoint') if (!validation.isValid) { @@ -37,6 +38,10 @@ export const azureAnthropicProvider: ProviderConfig = { }) throw new Error(`Invalid Azure Anthropic endpoint: ${validation.error}`) } + if (!validation.resolvedIP) { + throw new Error('Invalid Azure Anthropic endpoint: could not resolve a pinnable IP address') + } + pinnedFetch = createPinnedFetch(validation.resolvedIP) } const apiKey = request.apiKey @@ -67,6 +72,7 @@ export const azureAnthropicProvider: ProviderConfig = { new Anthropic({ baseURL, apiKey, + ...(pinnedFetch ? { fetch: pinnedFetch } : {}), defaultHeaders: { 'api-key': apiKey, 'anthropic-version': anthropicVersion, diff --git a/apps/sim/providers/azure-openai/index.test.ts b/apps/sim/providers/azure-openai/index.test.ts new file mode 100644 index 00000000000..7e18ea809df --- /dev/null +++ b/apps/sim/providers/azure-openai/index.test.ts @@ -0,0 +1,190 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProviderRequest } from '@/providers/types' + +const { + mockAzureOpenAI, + azureOpenAIArgs, + mockChatCreate, + mockValidate, + mockCreatePinnedFetch, + mockExecuteResponses, + sentinelFetch, + mockIsChatCompletionsEndpoint, + mockIsResponsesEndpoint, + envState, +} = vi.hoisted(() => { + const azureOpenAIArgs: Array> = [] + const sentinelFetch = vi.fn() + const mockChatCreate = vi.fn() + class MockAzureOpenAI { + chat = { completions: { create: mockChatCreate } } + constructor(opts: Record) { + azureOpenAIArgs.push(opts) + } + } + return { + mockAzureOpenAI: MockAzureOpenAI, + azureOpenAIArgs, + mockChatCreate, + mockValidate: vi.fn(), + mockCreatePinnedFetch: vi.fn(() => sentinelFetch), + mockExecuteResponses: vi.fn(), + sentinelFetch, + mockIsChatCompletionsEndpoint: vi.fn(() => false), + mockIsResponsesEndpoint: vi.fn(() => false), + envState: { + AZURE_OPENAI_ENDPOINT: undefined as string | undefined, + AZURE_OPENAI_API_VERSION: undefined as string | undefined, + }, + } +}) + +vi.mock('openai', () => ({ AzureOpenAI: mockAzureOpenAI })) +vi.mock('@/lib/core/config/env', () => ({ env: envState })) +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) +vi.mock('@/lib/core/security/input-validation.server', () => ({ + validateUrlWithDNS: mockValidate, + createPinnedFetch: mockCreatePinnedFetch, +})) +vi.mock('@/providers/openai/core', () => ({ + executeResponsesProviderRequest: mockExecuteResponses, +})) +vi.mock('@/providers/azure-openai/utils', () => ({ + isChatCompletionsEndpoint: mockIsChatCompletionsEndpoint, + isResponsesEndpoint: mockIsResponsesEndpoint, + extractBaseUrl: vi.fn((url: string) => url), + extractDeploymentFromUrl: vi.fn(() => null), + extractApiVersionFromUrl: vi.fn(() => null), + createReadableStreamFromAzureOpenAIStream: vi.fn(), + checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), +})) +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => []), + getProviderDefaultModel: vi.fn(() => 'azure/gpt-4o'), +})) +vi.mock('@/providers/attachments', () => ({ + prepareProviderAttachments: vi.fn(() => []), +})) +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })), + prepareToolsWithUsageControl: vi.fn(() => ({ + tools: [], + toolChoice: undefined, + forcedTools: [], + })), + sumToolCosts: vi.fn(() => 0), +})) +vi.mock('@/tools', () => ({ executeTool: vi.fn() })) + +import { azureOpenAIProvider } from '@/providers/azure-openai/index' + +function request(overrides: Partial): ProviderRequest { + return { model: 'azure/gpt-4o', apiKey: 'k', messages: [], ...overrides } +} + +/** Config object passed to the Responses core on the Nth call. */ +const responsesConfig = (call = 0) => mockExecuteResponses.mock.calls[call][1] + +describe('azureOpenAIProvider — SSRF pinning', () => { + beforeEach(() => { + vi.clearAllMocks() + azureOpenAIArgs.length = 0 + envState.AZURE_OPENAI_ENDPOINT = undefined + envState.AZURE_OPENAI_API_VERSION = undefined + mockIsChatCompletionsEndpoint.mockReturnValue(false) + mockIsResponsesEndpoint.mockReturnValue(false) + mockExecuteResponses.mockResolvedValue({ content: 'ok' }) + }) + + describe('Responses API path', () => { + it('validates and threads the pinned fetch into the Responses core for a user endpoint', async () => { + mockValidate.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + + await azureOpenAIProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + + expect(mockValidate).toHaveBeenCalledWith('https://rebind.attacker.tld', 'azureEndpoint') + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') + expect(responsesConfig().fetch).toBe(sentinelFetch) + }) + + it('passes no custom fetch when the endpoint comes from trusted server env', async () => { + envState.AZURE_OPENAI_ENDPOINT = 'https://trusted.openai.azure.com' + + await azureOpenAIProvider.executeRequest(request({ azureEndpoint: undefined })) + + expect(mockValidate).not.toHaveBeenCalled() + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(responsesConfig().fetch).toBeUndefined() + }) + + it('throws and never reaches the Responses core when validation blocks the endpoint', async () => { + mockValidate.mockResolvedValue({ isValid: false, error: 'resolves to a blocked IP address' }) + + await expect( + azureOpenAIProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + ).rejects.toThrow('Invalid Azure OpenAI endpoint') + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(mockExecuteResponses).not.toHaveBeenCalled() + }) + + it('fails closed when validation passes but yields no resolvable IP to pin', async () => { + mockValidate.mockResolvedValue({ isValid: true }) + + await expect( + azureOpenAIProvider.executeRequest( + request({ azureEndpoint: 'https://rebind.attacker.tld' }) + ) + ).rejects.toThrow('could not resolve a pinnable IP address') + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(mockExecuteResponses).not.toHaveBeenCalled() + }) + }) + + describe('Chat Completions path', () => { + it('constructs the AzureOpenAI client with the pinned fetch for a user endpoint', async () => { + mockIsChatCompletionsEndpoint.mockReturnValue(true) + mockValidate.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + mockChatCreate.mockResolvedValue({ + choices: [{ message: { content: 'hi', tool_calls: undefined } }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + + await azureOpenAIProvider.executeRequest( + request({ + azureEndpoint: 'https://rebind.attacker.tld/openai/deployments/gpt-4o/chat/completions', + }) + ) + + expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10') + expect(azureOpenAIArgs[0]).toMatchObject({ fetch: sentinelFetch }) + }) + + it('constructs the AzureOpenAI client without a custom fetch for a trusted env endpoint', async () => { + mockIsChatCompletionsEndpoint.mockReturnValue(true) + envState.AZURE_OPENAI_ENDPOINT = + 'https://trusted.openai.azure.com/openai/deployments/gpt-4o/chat/completions' + mockChatCreate.mockResolvedValue({ + choices: [{ message: { content: 'hi', tool_calls: undefined } }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + + await azureOpenAIProvider.executeRequest(request({ azureEndpoint: undefined })) + + expect(mockCreatePinnedFetch).not.toHaveBeenCalled() + expect(azureOpenAIArgs[0]).not.toHaveProperty('fetch') + }) + }) +}) diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index ddae32b7fb4..24d07184282 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -12,7 +12,7 @@ import type { } from 'openai/resources/chat/completions' import type { ReasoningEffort } from 'openai/resources/shared' import { env } from '@/lib/core/config/env' -import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' +import { createPinnedFetch, validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { prepareProviderAttachments } from '@/providers/attachments' @@ -56,7 +56,8 @@ async function executeChatCompletionsRequest( request: ProviderRequest, azureEndpoint: string, azureApiVersion: string, - deploymentName: string + deploymentName: string, + pinnedFetch?: typeof fetch ): Promise { logger.info('Using Azure OpenAI Chat Completions API', { model: request.model, @@ -75,6 +76,7 @@ async function executeChatCompletionsRequest( apiKey: request.apiKey!, apiVersion: azureApiVersion, endpoint: azureEndpoint, + ...(pinnedFetch ? { fetch: pinnedFetch } : {}), }) const allMessages: ChatCompletionMessageParam[] = [] @@ -606,6 +608,7 @@ export const azureOpenAIProvider: ProviderConfig = { ) } + let pinnedFetch: typeof fetch | undefined if (userProvidedEndpoint) { const validation = await validateUrlWithDNS(userProvidedEndpoint, 'azureEndpoint') if (!validation.isValid) { @@ -615,6 +618,10 @@ export const azureOpenAIProvider: ProviderConfig = { }) throw new Error(`Invalid Azure OpenAI endpoint: ${validation.error}`) } + if (!validation.resolvedIP) { + throw new Error('Invalid Azure OpenAI endpoint: could not resolve a pinnable IP address') + } + pinnedFetch = createPinnedFetch(validation.resolvedIP) } const apiKey = request.apiKey @@ -652,7 +659,8 @@ export const azureOpenAIProvider: ProviderConfig = { { ...request, apiKey }, baseUrl, azureApiVersion, - deploymentName + deploymentName, + pinnedFetch ) } @@ -676,6 +684,7 @@ export const azureOpenAIProvider: ProviderConfig = { 'api-key': apiKey, }, logger, + fetch: pinnedFetch, } ) } @@ -700,6 +709,7 @@ export const azureOpenAIProvider: ProviderConfig = { 'api-key': apiKey, }, logger, + fetch: pinnedFetch, } ) }, diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index c0fa50def86..913700ef5d7 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -41,6 +41,12 @@ export interface ResponsesProviderConfig { endpoint: string headers: Record logger: Logger + /** + * Optional fetch implementation. Used to pin the connection to a pre-validated + * IP (DNS-rebinding/SSRF protection) when the endpoint is user-supplied. + * Defaults to the global fetch. + */ + fetch?: typeof fetch } /** @@ -51,6 +57,7 @@ export async function executeResponsesProviderRequest( config: ResponsesProviderConfig ): Promise { const { logger } = config + const fetchImpl = config.fetch ?? fetch logger.info(`Preparing ${config.providerLabel} request`, { model: request.model, @@ -207,7 +214,7 @@ export async function executeResponsesProviderRequest( const postResponses = async ( body: Record ): Promise => { - const response = await fetch(config.endpoint, { + const response = await fetchImpl(config.endpoint, { method: 'POST', headers: config.headers, body: JSON.stringify(body), @@ -229,7 +236,7 @@ export async function executeResponsesProviderRequest( if (request.stream && (!tools || tools.length === 0)) { logger.info(`Using streaming response for ${config.providerLabel} request`) - const streamResponse = await fetch(config.endpoint, { + const streamResponse = await fetchImpl(config.endpoint, { method: 'POST', headers: config.headers, body: JSON.stringify(createRequestBody(initialInput, { stream: true })), @@ -643,7 +650,7 @@ export async function executeResponsesProviderRequest( } } - const streamResponse = await fetch(config.endpoint, { + const streamResponse = await fetchImpl(config.endpoint, { method: 'POST', headers: config.headers, body: JSON.stringify(createRequestBody(currentInput, streamOverrides)), From 39d0b56e9154735a02489e349792e1a98fc6e612 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 15:16:41 -0700 Subject: [PATCH 04/24] refactor(table): split the 5.3k-line service.ts god-file into per-concern modules (#5069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(table): extract row ordering, executions, and tx helpers from service.ts Move the row position/fractional-ordering internals to rows/ordering.ts, the row-execution (workflow-group result) internals to rows/executions.ts, and the shared tx-timeout helpers to tx.ts. Pure code-motion — verbatim bodies, identical behavior. service.ts: 5324 -> 4442 lines. * refactor(table): extract row CRUD and query into rows/service.ts Move insert/update/upsert/delete/replace/batch row writes, queryRows/ getRowById reads, and findRowMatches into rows/service.ts. Verbatim bodies; consumers repointed; @/lib/table barrel re-exports the new module so callers are unchanged. service.ts: 4442 -> 2788 lines. * refactor(table): extract column/schema management into columns/service.ts Move add/rename/delete column ops and column-type/constraint updates into columns/service.ts. addTableColumnsWithTx stays in service.ts (table-creation primitive) to avoid a cycle. Verbatim bodies; barrel re-exports the module. service.ts: 2788 -> 2149 lines. * refactor(table): extract job state machine and export jobs into jobs/service.ts Move tableJobs reads/mapping, the job lifecycle state machine, and export-job queries into jobs/service.ts. service.ts imports latestJob* one-way for table metadata enrichment (no cycle). Import-data orchestration helpers stay in service.ts for now. Verbatim bodies. service.ts: 2149 -> 1791 lines. * refactor(table): extract workflow-group management into workflow-groups/service.ts Move add/update/delete workflow groups + outputs and pruneStale into workflow-groups/service.ts, preserving the dynamic backfill-runner import (cycle-breaker). Verbatim bodies; no cycle. service.ts: 1791 -> 851 lines. * refactor(table): extract import-job data ops into import-data.ts Move bulk insert, schema setup, and append/replace import operations into import-data.ts (consumed by import-runner + import route). service.ts is now the pure table-entity module (root CRUD + shared lock/column primitives). Verbatim bodies; no cycle. service.ts: 851 -> 664 lines (5324 at start). * refactor(table): restore private visibility and drop dead helpers post-split Un-export DerivedJobFields/JOB_PROJECTION/mapJobRow (file-local in jobs/service.ts, were private pre-split) so they no longer leak into the @/lib/table barrel. Remove dead code carried into the new modules: countTables (createTable does its own inline count check) and the unused buildOrderedRowValues/OrderedRowValue pair. * refactor(table): use absolute imports throughout and fix an orphaned TSDoc Normalize all relative imports under lib/table to absolute @/lib/table/... per the project import rule (the split modules and a few pre-existing holdouts), and relocate the getTableById TSDoc that had drifted above applyColumnOrderToSchema. Comment/import-only; zero behavior change. --- .../[tableId]/delete-async/route.test.ts | 2 +- .../api/table/[tableId]/delete-async/route.ts | 2 +- .../[tableId]/export-async/route.test.ts | 2 +- .../api/table/[tableId]/export-async/route.ts | 2 +- .../[tableId]/export/download/route.test.ts | 2 +- .../table/[tableId]/export/download/route.ts | 2 +- .../api/table/[tableId]/export/route.test.ts | 2 +- .../app/api/table/[tableId]/export/route.ts | 2 +- .../app/api/table/[tableId]/groups/route.ts | 6 +- .../[tableId]/import-async/route.test.ts | 2 +- .../api/table/[tableId]/import-async/route.ts | 2 +- .../api/table/[tableId]/import/route.test.ts | 20 +- .../app/api/table/[tableId]/import/route.ts | 3 +- .../table/[tableId]/job/cancel/route.test.ts | 2 +- .../api/table/[tableId]/job/cancel/route.ts | 2 +- .../table/[tableId]/rows/find/route.test.ts | 2 +- .../api/table/[tableId]/rows/find/route.ts | 2 +- .../api/table/[tableId]/rows/route.test.ts | 2 +- .../sim/app/api/table/[tableId]/rows/route.ts | 2 +- .../app/api/table/import-csv/route.test.ts | 5 +- apps/sim/app/api/table/jobs/route.ts | 2 +- .../app/api/v1/tables/[tableId]/rows/route.ts | 2 +- apps/sim/background/resume-execution.ts | 5 +- .../background/workflow-column-execution.ts | 12 +- .../lib/copilot/request/tools/tables.test.ts | 3 + apps/sim/lib/copilot/request/tools/tables.ts | 3 +- .../tools/handlers/function-execute.ts | 3 +- .../tools/server/table/user-table.test.ts | 33 +- .../copilot/tools/server/table/user-table.ts | 31 +- .../table/__tests__/find-row-matches.test.ts | 2 +- .../lib/table/__tests__/lock-order.test.ts | 2 +- .../service-filter-threading.test.ts | 2 +- .../lib/table/__tests__/update-row.test.ts | 5 +- .../lib/table/__tests__/validation.test.ts | 4 +- apps/sim/lib/table/backfill-runner.ts | 10 +- apps/sim/lib/table/billing.ts | 2 +- apps/sim/lib/table/cell-write.ts | 3 +- apps/sim/lib/table/column-keys.ts | 9 +- apps/sim/lib/table/column-naming.ts | 2 +- apps/sim/lib/table/columns/service.ts | 668 +++ apps/sim/lib/table/delete-runner.test.ts | 8 +- apps/sim/lib/table/delete-runner.ts | 7 +- apps/sim/lib/table/deps.ts | 8 +- apps/sim/lib/table/dispatcher.ts | 4 +- apps/sim/lib/table/export-runner.test.ts | 2 + apps/sim/lib/table/export-runner.ts | 4 +- apps/sim/lib/table/hooks/index.ts | 2 +- apps/sim/lib/table/hooks/use-table-columns.ts | 2 +- apps/sim/lib/table/import-data.ts | 200 + apps/sim/lib/table/import-runner.ts | 11 +- apps/sim/lib/table/index.ts | 25 +- apps/sim/lib/table/jobs/service.ts | 383 ++ apps/sim/lib/table/llm/enrichment.ts | 2 +- apps/sim/lib/table/llm/index.ts | 2 +- apps/sim/lib/table/llm/wand.ts | 2 +- apps/sim/lib/table/query-builder/constants.ts | 2 +- .../sim/lib/table/query-builder/converters.ts | 9 +- apps/sim/lib/table/query-builder/index.ts | 6 +- .../table/query-builder/use-query-builder.ts | 4 +- apps/sim/lib/table/rows/executions.ts | 298 ++ apps/sim/lib/table/rows/ordering.ts | 557 ++ apps/sim/lib/table/rows/service.ts | 1676 ++++++ apps/sim/lib/table/service.ts | 4702 +---------------- apps/sim/lib/table/sql.ts | 12 +- apps/sim/lib/table/tx.ts | 50 + apps/sim/lib/table/types.ts | 2 +- apps/sim/lib/table/validation.ts | 14 +- apps/sim/lib/table/workflow-columns.ts | 21 +- apps/sim/lib/table/workflow-groups/service.ts | 964 ++++ apps/sim/lib/workflows/deployment-outbox.ts | 2 +- 70 files changed, 5027 insertions(+), 4823 deletions(-) create mode 100644 apps/sim/lib/table/columns/service.ts create mode 100644 apps/sim/lib/table/import-data.ts create mode 100644 apps/sim/lib/table/jobs/service.ts create mode 100644 apps/sim/lib/table/rows/executions.ts create mode 100644 apps/sim/lib/table/rows/ordering.ts create mode 100644 apps/sim/lib/table/rows/service.ts create mode 100644 apps/sim/lib/table/tx.ts create mode 100644 apps/sim/lib/table/workflow-groups/service.ts diff --git a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts index 9565725c8a6..3ebdd6a768a 100644 --- a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts @@ -28,7 +28,7 @@ vi.mock('@sim/utils/id', () => ({ generateId: vi.fn().mockReturnValue('job-id-xyz'), generateShortId: vi.fn().mockReturnValue('short-id'), })) -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/jobs/service', () => ({ markTableJobRunning: mockMarkTableJobRunning, releaseJobClaim: mockReleaseJobClaim, })) diff --git a/apps/sim/app/api/table/[tableId]/delete-async/route.ts b/apps/sim/app/api/table/[tableId]/delete-async/route.ts index 7dcd8c37676..a7ab365fee9 100644 --- a/apps/sim/app/api/table/[tableId]/delete-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/delete-async/route.ts @@ -9,7 +9,7 @@ import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { markTableDeleteFailed, runTableDelete } from '@/lib/table/delete-runner' -import { markTableJobRunning, releaseJobClaim } from '@/lib/table/service' +import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service' import type { TableDeleteJobPayload } from '@/lib/table/types' import { accessError, checkAccess, tableFilterError } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/[tableId]/export-async/route.test.ts b/apps/sim/app/api/table/[tableId]/export-async/route.test.ts index 177e02abf37..5be95a85ee6 100644 --- a/apps/sim/app/api/table/[tableId]/export-async/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/export-async/route.test.ts @@ -16,7 +16,7 @@ vi.mock('@sim/utils/id', () => ({ generateId: vi.fn().mockReturnValue('job-id-xyz'), generateShortId: vi.fn().mockReturnValue('short-id'), })) -vi.mock('@/lib/table/service', () => ({ markTableJobRunning: mockMarkTableJobRunning })) +vi.mock('@/lib/table/jobs/service', () => ({ markTableJobRunning: mockMarkTableJobRunning })) vi.mock('@/lib/table/export-runner', () => ({ runTableExport: mockRunTableExport })) vi.mock('@/lib/core/utils/background', () => ({ runDetached: (_label: string, work: () => Promise) => { diff --git a/apps/sim/app/api/table/[tableId]/export-async/route.ts b/apps/sim/app/api/table/[tableId]/export-async/route.ts index 26ded9b6e1d..1218af5c800 100644 --- a/apps/sim/app/api/table/[tableId]/export-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/export-async/route.ts @@ -9,7 +9,7 @@ import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { runTableExport, type TableExportPayload } from '@/lib/table/export-runner' -import { markTableJobRunning, releaseJobClaim } from '@/lib/table/service' +import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service' import type { TableExportJobPayload } from '@/lib/table/types' import { accessError, checkAccess } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/[tableId]/export/download/route.test.ts b/apps/sim/app/api/table/[tableId]/export/download/route.test.ts index c3458093e68..53ed1a28cf3 100644 --- a/apps/sim/app/api/table/[tableId]/export/download/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/export/download/route.test.ts @@ -12,7 +12,7 @@ const { mockCheckAccess, mockGetTableJob, mockGeneratePresignedDownloadUrl } = v mockGeneratePresignedDownloadUrl: vi.fn(), })) -vi.mock('@/lib/table/service', () => ({ getTableJob: mockGetTableJob })) +vi.mock('@/lib/table/jobs/service', () => ({ getTableJob: mockGetTableJob })) vi.mock('@/lib/uploads/core/storage-service', () => ({ generatePresignedDownloadUrl: mockGeneratePresignedDownloadUrl, })) diff --git a/apps/sim/app/api/table/[tableId]/export/download/route.ts b/apps/sim/app/api/table/[tableId]/export/download/route.ts index 577c2747b8c..988b774a262 100644 --- a/apps/sim/app/api/table/[tableId]/export/download/route.ts +++ b/apps/sim/app/api/table/[tableId]/export/download/route.ts @@ -5,7 +5,7 @@ import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getTableJob } from '@/lib/table/service' +import { getTableJob } from '@/lib/table/jobs/service' import type { TableExportJobPayload } from '@/lib/table/types' import { generatePresignedDownloadUrl } from '@/lib/uploads/core/storage-service' import { accessError, checkAccess } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/[tableId]/export/route.test.ts b/apps/sim/app/api/table/[tableId]/export/route.test.ts index 365ae58aca4..b420b5f97da 100644 --- a/apps/sim/app/api/table/[tableId]/export/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/export/route.test.ts @@ -20,7 +20,7 @@ vi.mock('@/app/api/table/utils', async () => { } }) -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/rows/service', () => ({ queryRows: mockQueryRows, })) diff --git a/apps/sim/app/api/table/[tableId]/export/route.ts b/apps/sim/app/api/table/[tableId]/export/route.ts index 096f9d6c5af..75208e3d982 100644 --- a/apps/sim/app/api/table/[tableId]/export/route.ts +++ b/apps/sim/app/api/table/[tableId]/export/route.ts @@ -7,7 +7,7 @@ import { neutralizeCsvFormula } from '@/lib/core/utils/csv' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildNameById, getColumnId, rowDataIdToName } from '@/lib/table/column-keys' -import { queryRows } from '@/lib/table/service' +import { queryRows } from '@/lib/table/rows/service' import { accessError, checkAccess } from '@/app/api/table/utils' const logger = createLogger('TableExport') diff --git a/apps/sim/app/api/table/[tableId]/groups/route.ts b/apps/sim/app/api/table/[tableId]/groups/route.ts index b2ce9b54be6..e5c6400d3df 100644 --- a/apps/sim/app/api/table/[tableId]/groups/route.ts +++ b/apps/sim/app/api/table/[tableId]/groups/route.ts @@ -9,7 +9,11 @@ import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { addWorkflowGroup, deleteWorkflowGroup, updateWorkflowGroup } from '@/lib/table/service' +import { + addWorkflowGroup, + deleteWorkflowGroup, + updateWorkflowGroup, +} from '@/lib/table/workflow-groups/service' import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils' const logger = createLogger('TableWorkflowGroupsAPI') diff --git a/apps/sim/app/api/table/[tableId]/import-async/route.test.ts b/apps/sim/app/api/table/[tableId]/import-async/route.test.ts index 7ed47fa66e3..271cf2b8ed6 100644 --- a/apps/sim/app/api/table/[tableId]/import-async/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/import-async/route.test.ts @@ -16,7 +16,7 @@ vi.mock('@sim/utils/id', () => ({ generateId: vi.fn().mockReturnValue('import-id-xyz'), generateShortId: vi.fn().mockReturnValue('short-id'), })) -vi.mock('@/lib/table/service', () => ({ markTableJobRunning: mockMarkTableImporting })) +vi.mock('@/lib/table/jobs/service', () => ({ markTableJobRunning: mockMarkTableImporting })) vi.mock('@/lib/table/import-runner', () => ({ runTableImport: mockRunTableImport })) vi.mock('@/lib/core/utils/background', () => ({ runDetached: (_label: string, work: () => Promise) => { diff --git a/apps/sim/app/api/table/[tableId]/import-async/route.ts b/apps/sim/app/api/table/[tableId]/import-async/route.ts index f256bf5f35a..ba3787a4fbc 100644 --- a/apps/sim/app/api/table/[tableId]/import-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/import-async/route.ts @@ -9,7 +9,7 @@ import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { runTableImport, type TableImportPayload } from '@/lib/table/import-runner' -import { markTableJobRunning, releaseJobClaim } from '@/lib/table/service' +import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service' import { accessError, checkAccess } from '@/app/api/table/utils' const logger = createLogger('TableImportIntoAsync') diff --git a/apps/sim/app/api/table/[tableId]/import/route.test.ts b/apps/sim/app/api/table/[tableId]/import/route.test.ts index 76650baf4c1..588e1813b0a 100644 --- a/apps/sim/app/api/table/[tableId]/import/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/import/route.test.ts @@ -45,20 +45,26 @@ vi.mock('@/app/api/table/utils', async () => { }) /** - * The route imports `importAppendRows` / `importReplaceRows` from the barrel, - * which forwards them from `./service`. These functions own the import - * transaction (column adds + row writes); mocking the service module replaces - * them without touching the other real helpers (`coerceRowsForTable`, - * `createCsvParser`, etc.) exported through the barrel. + * The route imports `importAppendRows` / `importReplaceRows` from + * `@/lib/table/import-data`. These functions own the import transaction (column + * adds + row writes); mocking that module replaces them without touching the + * other real helpers (`coerceRowsForTable`, `createCsvParser`, etc.) exported + * through the barrel. */ -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/import-data', () => ({ importAppendRows: mockImportAppendRows, importReplaceRows: mockImportReplaceRows, - dispatchAfterBatchInsert: mockDispatchAfterBatchInsert, +})) + +vi.mock('@/lib/table/jobs/service', () => ({ markTableJobRunning: mockMarkTableImporting, releaseJobClaim: mockReleaseImportClaim, })) +vi.mock('@/lib/table/rows/service', () => ({ + dispatchAfterBatchInsert: mockDispatchAfterBatchInsert, +})) + import { POST } from '@/app/api/table/[tableId]/import/route' function createCsvFile(contents: string, name = 'data.csv', type = 'text/csv'): File { diff --git a/apps/sim/app/api/table/[tableId]/import/route.ts b/apps/sim/app/api/table/[tableId]/import/route.ts index ef57b09aced..fc4a321a3cf 100644 --- a/apps/sim/app/api/table/[tableId]/import/route.ts +++ b/apps/sim/app/api/table/[tableId]/import/route.ts @@ -25,8 +25,6 @@ import { createCsvParser, dispatchAfterBatchInsert, generateColumnId, - importAppendRows, - importReplaceRows, inferColumnType, markTableJobRunning, releaseJobClaim, @@ -35,6 +33,7 @@ import { type TableSchema, validateMapping, } from '@/lib/table' +import { importAppendRows, importReplaceRows } from '@/lib/table/import-data' import { accessError, checkAccess, diff --git a/apps/sim/app/api/table/[tableId]/job/cancel/route.test.ts b/apps/sim/app/api/table/[tableId]/job/cancel/route.test.ts index f1837b42dc7..dd0a16b2576 100644 --- a/apps/sim/app/api/table/[tableId]/job/cancel/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/job/cancel/route.test.ts @@ -15,7 +15,7 @@ const { mockCheckAccess, mockMarkJobCanceled, mockGetTableJob, mockAppendTableEv }) ) -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/jobs/service', () => ({ markJobCanceled: mockMarkJobCanceled, getTableJob: mockGetTableJob, })) diff --git a/apps/sim/app/api/table/[tableId]/job/cancel/route.ts b/apps/sim/app/api/table/[tableId]/job/cancel/route.ts index b4ee3d98346..677c338f65b 100644 --- a/apps/sim/app/api/table/[tableId]/job/cancel/route.ts +++ b/apps/sim/app/api/table/[tableId]/job/cancel/route.ts @@ -6,7 +6,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { appendTableEvent } from '@/lib/table/events' -import { getTableJob, markJobCanceled } from '@/lib/table/service' +import { getTableJob, markJobCanceled } from '@/lib/table/jobs/service' import type { TableJobType } from '@/lib/table/types' import { accessError, checkAccess } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/[tableId]/rows/find/route.test.ts b/apps/sim/app/api/table/[tableId]/rows/find/route.test.ts index a2d0031a049..139427066cb 100644 --- a/apps/sim/app/api/table/[tableId]/rows/find/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/rows/find/route.test.ts @@ -23,7 +23,7 @@ vi.mock('@/app/api/table/utils', async () => { } }) -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/rows/service', () => ({ findRowMatches: mockFindRowMatches, })) diff --git a/apps/sim/app/api/table/[tableId]/rows/find/route.ts b/apps/sim/app/api/table/[tableId]/rows/find/route.ts index e909db2eaa8..77a476ee145 100644 --- a/apps/sim/app/api/table/[tableId]/rows/find/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/find/route.ts @@ -6,7 +6,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { Sort } from '@/lib/table' -import { findRowMatches } from '@/lib/table/service' +import { findRowMatches } from '@/lib/table/rows/service' import { TableQueryValidationError } from '@/lib/table/sql' import { accessError, checkAccess } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/[tableId]/rows/route.test.ts b/apps/sim/app/api/table/[tableId]/rows/route.test.ts index 23a12376e03..8c5fbab9276 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.test.ts @@ -40,7 +40,7 @@ vi.mock('@/lib/table', async () => { } }) -vi.mock('@/lib/table/service', () => ({ +vi.mock('@/lib/table/rows/service', () => ({ queryRows: mockQueryRows, })) diff --git a/apps/sim/app/api/table/[tableId]/rows/route.ts b/apps/sim/app/api/table/[tableId]/rows/route.ts index 372cd758041..ca239534696 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.ts @@ -25,7 +25,7 @@ import { validateRowData, validateRowSize, } from '@/lib/table' -import { queryRows } from '@/lib/table/service' +import { queryRows } from '@/lib/table/rows/service' import { TableQueryValidationError } from '@/lib/table/sql' import { rowWireTranslators } from '@/app/api/table/row-wire' import { accessError, checkAccess, rowWriteErrorResponse } from '@/app/api/table/utils' diff --git a/apps/sim/app/api/table/import-csv/route.test.ts b/apps/sim/app/api/table/import-csv/route.test.ts index dc0bb0a53a5..350f39241bb 100644 --- a/apps/sim/app/api/table/import-csv/route.test.ts +++ b/apps/sim/app/api/table/import-csv/route.test.ts @@ -22,9 +22,12 @@ vi.mock('@sim/utils/id', () => ({ // streaming multipart + CSV pipeline is exercised end-to-end. vi.mock('@/lib/table/service', () => ({ createTable: mockCreateTable, - batchInsertRows: mockBatchInsertRows, deleteTable: mockDeleteTable, })) + +vi.mock('@/lib/table/rows/service', () => ({ + batchInsertRows: mockBatchInsertRows, +})) vi.mock('@/lib/table/billing', () => ({ getWorkspaceTableLimits: mockGetLimits })) vi.mock('@/app/api/table/utils', async () => { const { NextResponse } = await import('next/server') diff --git a/apps/sim/app/api/table/jobs/route.ts b/apps/sim/app/api/table/jobs/route.ts index 912d769c39f..dbe38d3e489 100644 --- a/apps/sim/app/api/table/jobs/route.ts +++ b/apps/sim/app/api/table/jobs/route.ts @@ -5,7 +5,7 @@ import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { listWorkspaceExportJobs } from '@/lib/table/service' +import { listWorkspaceExportJobs } from '@/lib/table/jobs/service' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('TableJobsAPI') diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts index bce536fc9fb..5f637c3a26a 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/route.ts @@ -31,7 +31,7 @@ import { validateRowData, validateRowSize, } from '@/lib/table' -import { queryRows } from '@/lib/table/service' +import { queryRows } from '@/lib/table/rows/service' import { TableQueryValidationError } from '@/lib/table/sql' import { accessError, checkAccess, rowWriteErrorResponse } from '@/app/api/table/utils' import { diff --git a/apps/sim/background/resume-execution.ts b/apps/sim/background/resume-execution.ts index c76fd97b136..f1629cf2674 100644 --- a/apps/sim/background/resume-execution.ts +++ b/apps/sim/background/resume-execution.ts @@ -50,7 +50,7 @@ export async function executeResumeJob(payload: ResumeExecutionPayload) { // philosophy). Aborting here also stops the wasted compute the guard alone // can't prevent. Read the cell's current exec and bail if cancelled. if (cellContext) { - const { getRowById } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/rows/service') const cellRow = await getRowById( cellContext.tableId, cellContext.rowId, @@ -323,7 +323,8 @@ async function continueCascadeAfterResume(cellContext: { workspaceId: string groupId: string }): Promise { - const { getTableById, getRowById } = await import('@/lib/table/service') + const { getTableById } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/rows/service') const { pickNextEligibleGroupForRow } = await import('@/lib/table/workflow-columns') const { runRowCascadeLoop } = await import('@/background/workflow-column-execution') diff --git a/apps/sim/background/workflow-column-execution.ts b/apps/sim/background/workflow-column-execution.ts index bba438c56dd..36db4f0e15c 100644 --- a/apps/sim/background/workflow-column-execution.ts +++ b/apps/sim/background/workflow-column-execution.ts @@ -51,7 +51,8 @@ export async function executeWorkflowGroupCellJob( signal?: AbortSignal ) { const { tableId, rowId, workspaceId } = payload - const { getTableById, getRowById } = await import('@/lib/table/service') + const { getTableById } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/rows/service') const { pickNextEligibleGroupForRow } = await import('@/lib/table/workflow-columns') let currentPayload = payload @@ -105,7 +106,8 @@ export async function runRowCascadeLoop( signal?: AbortSignal ): Promise<'blocked' | undefined> { const { tableId, rowId, workspaceId } = payload - const { getTableById, getRowById } = await import('@/lib/table/service') + const { getTableById } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/rows/service') const { pickNextEligibleGroupForRow } = await import('@/lib/table/workflow-columns') let currentGroupId = payload.groupId @@ -175,7 +177,7 @@ async function runWorkflowAndWriteTerminal( const requestId = `wfgrp-${executionId}` return runWithRequestContext({ requestId }, async () => { - const { getRowById } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/rows/service') const { executeWorkflow } = await import('@/lib/workflows/executor/execute-workflow') const { loadWorkflowFromNormalizedTables, loadDeployedWorkflowState } = await import( '@/lib/workflows/persistence/utils' @@ -258,7 +260,7 @@ async function runWorkflowAndWriteTerminal( logger.warn( `Usage limit reached — halting enrichment (table=${tableId} row=${rowId} group=${groupId})` ) - const { updateRow } = await import('@/lib/table/service') + const { updateRow } = await import('@/lib/table/rows/service') await updateRow( { tableId, rowId, data: {}, workspaceId, executionsPatch: { [groupId]: null } }, table, @@ -525,7 +527,7 @@ async function runWorkflowAndWriteTerminal( // cell's exec so it reverts to un-run (no error/cancelled badge — // matching "don't mark"; re-runnable after upgrade). Each blocked // cell clears its own. - const { updateRow } = await import('@/lib/table/service') + const { updateRow } = await import('@/lib/table/rows/service') await updateRow( { tableId, rowId, data: {}, workspaceId, executionsPatch: { [groupId]: null } }, table, diff --git a/apps/sim/lib/copilot/request/tools/tables.test.ts b/apps/sim/lib/copilot/request/tools/tables.test.ts index e8a5be5877b..17c0544dcd4 100644 --- a/apps/sim/lib/copilot/request/tools/tables.test.ts +++ b/apps/sim/lib/copilot/request/tools/tables.test.ts @@ -12,6 +12,9 @@ const { mockGetTableById, mockReplaceTableRows } = vi.hoisted(() => ({ vi.mock('@/lib/table/service', () => ({ getTableById: mockGetTableById, +})) + +vi.mock('@/lib/table/rows/service', () => ({ replaceTableRows: mockReplaceTableRows, })) diff --git a/apps/sim/lib/copilot/request/tools/tables.ts b/apps/sim/lib/copilot/request/tools/tables.ts index ec1b72d6fa5..3db2ab18c60 100644 --- a/apps/sim/lib/copilot/request/tools/tables.ts +++ b/apps/sim/lib/copilot/request/tools/tables.ts @@ -11,7 +11,8 @@ import { withCopilotSpan } from '@/lib/copilot/request/otel' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import type { RowData, TableDefinition } from '@/lib/table' import { buildIdByName, rowDataNameToId } from '@/lib/table/column-keys' -import { getTableById, replaceTableRows } from '@/lib/table/service' +import { replaceTableRows } from '@/lib/table/rows/service' +import { getTableById } from '@/lib/table/service' const logger = createLogger('CopilotToolResultTables') diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 3df97818c0e..725a2d2391d 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -3,7 +3,8 @@ import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/ import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases' import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' -import { getTableById, listTables, queryRows } from '@/lib/table/service' +import { queryRows } from '@/lib/table/rows/service' +import { getTableById, listTables } from '@/lib/table/service' import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { fetchWorkspaceFileBuffer, diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts index 8ad2e0d6b2b..7d90e9c00a3 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts @@ -56,26 +56,39 @@ vi.mock('@/enrichments/registry', () => ({ })) vi.mock('@/lib/table/service', () => ({ - addTableColumn: vi.fn(), - addWorkflowGroup: mockAddWorkflowGroup, - batchInsertRows: mockBatchInsertRows, - batchUpdateRows: vi.fn(), createTable: mockCreateTable, + deleteTable: mockDeleteTable, + getTableById: mockGetTableById, + renameTable: vi.fn(), +})) + +vi.mock('@/lib/table/workflow-groups/service', () => ({ + addWorkflowGroup: mockAddWorkflowGroup, + addWorkflowGroupOutput: vi.fn(), + deleteWorkflowGroup: vi.fn(), + deleteWorkflowGroupOutput: vi.fn(), + updateWorkflowGroup: vi.fn(), +})) + +vi.mock('@/lib/table/columns/service', () => ({ + addTableColumn: vi.fn(), deleteColumn: vi.fn(), deleteColumns: vi.fn(), + renameColumn: vi.fn(), + updateColumnConstraints: vi.fn(), + updateColumnType: vi.fn(), +})) + +vi.mock('@/lib/table/rows/service', () => ({ + batchInsertRows: mockBatchInsertRows, + batchUpdateRows: vi.fn(), deleteRow: vi.fn(), deleteRowsByFilter: vi.fn(), deleteRowsByIds: vi.fn(), - deleteTable: mockDeleteTable, getRowById: vi.fn(), - getTableById: mockGetTableById, insertRow: vi.fn(), queryRows: vi.fn(), - renameColumn: vi.fn(), - renameTable: vi.fn(), replaceTableRows: mockReplaceTableRows, - updateColumnConstraints: vi.fn(), - updateColumnType: vi.fn(), updateRow: vi.fn(), updateRowsByFilter: vi.fn(), })) diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index 07ca1ee82e1..7614dc63aea 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -30,32 +30,26 @@ import { import { columnTypeForLeaf, deriveOutputColumnName } from '@/lib/table/column-naming' import { addTableColumn, - addWorkflowGroup, - addWorkflowGroupOutput, - batchInsertRows, - batchUpdateRows, - createTable, deleteColumn, deleteColumns, + renameColumn, + updateColumnConstraints, + updateColumnType, +} from '@/lib/table/columns/service' +import { + batchInsertRows, + batchUpdateRows, deleteRow, deleteRowsByFilter, deleteRowsByIds, - deleteTable, - deleteWorkflowGroup, - deleteWorkflowGroupOutput, getRowById, - getTableById, insertRow, queryRows, - renameColumn, - renameTable, replaceTableRows, - updateColumnConstraints, - updateColumnType, updateRow, updateRowsByFilter, - updateWorkflowGroup, -} from '@/lib/table/service' +} from '@/lib/table/rows/service' +import { createTable, deleteTable, getTableById, renameTable } from '@/lib/table/service' import type { ColumnDefinition, RowData, @@ -67,6 +61,13 @@ import type { WorkflowGroupOutput, } from '@/lib/table/types' import { cancelWorkflowGroupRuns, runWorkflowColumn } from '@/lib/table/workflow-columns' +import { + addWorkflowGroup, + addWorkflowGroupOutput, + deleteWorkflowGroup, + deleteWorkflowGroupOutput, + updateWorkflowGroup, +} from '@/lib/table/workflow-groups/service' import { fetchWorkspaceFileBuffer, resolveWorkspaceFileReference, diff --git a/apps/sim/lib/table/__tests__/find-row-matches.test.ts b/apps/sim/lib/table/__tests__/find-row-matches.test.ts index cbc1276888e..076a50686df 100644 --- a/apps/sim/lib/table/__tests__/find-row-matches.test.ts +++ b/apps/sim/lib/table/__tests__/find-row-matches.test.ts @@ -38,7 +38,7 @@ vi.mock('@/lib/table/validation', () => ({ checkBatchUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })), })) -import { findRowMatches } from '@/lib/table/service' +import { findRowMatches } from '@/lib/table/rows/service' import { buildFilterClause, buildSortClause } from '@/lib/table/sql' const COLUMNS: ColumnDefinition[] = [ diff --git a/apps/sim/lib/table/__tests__/lock-order.test.ts b/apps/sim/lib/table/__tests__/lock-order.test.ts index cc7f4ec9726..51c66714df5 100644 --- a/apps/sim/lib/table/__tests__/lock-order.test.ts +++ b/apps/sim/lib/table/__tests__/lock-order.test.ts @@ -10,7 +10,7 @@ import { userTableDefinitions } from '@sim/db/schema' import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { importAppendRows } from '@/lib/table/service' +import { importAppendRows } from '@/lib/table/import-data' import type { TableDefinition } from '@/lib/table/types' vi.mock('@sim/db', () => dbChainMock) diff --git a/apps/sim/lib/table/__tests__/service-filter-threading.test.ts b/apps/sim/lib/table/__tests__/service-filter-threading.test.ts index a09d9630edf..c0ade663506 100644 --- a/apps/sim/lib/table/__tests__/service-filter-threading.test.ts +++ b/apps/sim/lib/table/__tests__/service-filter-threading.test.ts @@ -43,7 +43,7 @@ vi.mock('@/lib/table/validation', () => ({ checkBatchUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })), })) -import { deleteRowsByFilter, queryRows, updateRowsByFilter } from '@/lib/table/service' +import { deleteRowsByFilter, queryRows, updateRowsByFilter } from '@/lib/table/rows/service' const COLUMNS: ColumnDefinition[] = [ { name: 'name', type: 'string' }, diff --git a/apps/sim/lib/table/__tests__/update-row.test.ts b/apps/sim/lib/table/__tests__/update-row.test.ts index 38a74695c3d..e43fa16a4bb 100644 --- a/apps/sim/lib/table/__tests__/update-row.test.ts +++ b/apps/sim/lib/table/__tests__/update-row.test.ts @@ -3,15 +3,14 @@ */ import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { deleteColumn, renameColumn } from '@/lib/table/columns/service' import { batchInsertRows, - deleteColumn, insertRow, - renameColumn, replaceTableRows, updateRow, upsertRow, -} from '@/lib/table/service' +} from '@/lib/table/rows/service' import type { TableDefinition } from '@/lib/table/types' import { getUniqueColumns } from '@/lib/table/validation' diff --git a/apps/sim/lib/table/__tests__/validation.test.ts b/apps/sim/lib/table/__tests__/validation.test.ts index 3c9a139f7a8..4ebfe9a6ffa 100644 --- a/apps/sim/lib/table/__tests__/validation.test.ts +++ b/apps/sim/lib/table/__tests__/validation.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { TABLE_LIMITS } from '../constants' +import { TABLE_LIMITS } from '@/lib/table/constants' import { type ColumnDefinition, coerceRowToSchema, @@ -15,7 +15,7 @@ import { validateTableName, validateTableSchema, validateUniqueConstraints, -} from '../validation' +} from '@/lib/table/validation' describe('Validation', () => { describe('validateTableName', () => { diff --git a/apps/sim/lib/table/backfill-runner.ts b/apps/sim/lib/table/backfill-runner.ts index cbaf6e640f2..f9e00bdb464 100644 --- a/apps/sim/lib/table/backfill-runner.ts +++ b/apps/sim/lib/table/backfill-runner.ts @@ -10,20 +10,20 @@ import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/co import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { appendTableEvent } from '@/lib/table/events' import { - batchUpdateRows, - getTableById, markJobFailed, markJobReady, markTableJobRunning, updateJobProgress, -} from '@/lib/table/service' +} from '@/lib/table/jobs/service' +import { pluckByPath } from '@/lib/table/pluck' +import { batchUpdateRows } from '@/lib/table/rows/service' +import { getTableById } from '@/lib/table/service' import type { RowData, TableBackfillJobPayload, TableDefinition, WorkflowGroupOutput, } from '@/lib/table/types' -import { pluckByPath } from './pluck' const logger = createLogger('TableBackfillRunner') @@ -330,7 +330,7 @@ export async function maybeBackfillGroupOutputs(opts: { // Release the claim so a ghost `running` job doesn't block imports/deletes. // Swallowed (warn only): a failed backfill never fails the schema change — // the data stays backfillable. - const { releaseJobClaim } = await import('./service') + const { releaseJobClaim } = await import('@/lib/table/jobs/service') await releaseJobClaim(table.id, jobId).catch(() => {}) logger.warn( `[${requestId}] Backfill dispatch failed for table ${table.id} group ${groupId}; skipping`, diff --git a/apps/sim/lib/table/billing.ts b/apps/sim/lib/table/billing.ts index 2efbe5f781a..2dfbc30cd93 100644 --- a/apps/sim/lib/table/billing.ts +++ b/apps/sim/lib/table/billing.ts @@ -7,8 +7,8 @@ import { createLogger } from '@sim/logger' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { getPlanTypeForLimits } from '@/lib/billing/plan-helpers' +import { getTablePlanLimits, type PlanName, type TablePlanLimits } from '@/lib/table/constants' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' -import { getTablePlanLimits, type PlanName, type TablePlanLimits } from './constants' const logger = createLogger('TableBilling') diff --git a/apps/sim/lib/table/cell-write.ts b/apps/sim/lib/table/cell-write.ts index c5caf6dc2dc..2d64a186938 100644 --- a/apps/sim/lib/table/cell-write.ts +++ b/apps/sim/lib/table/cell-write.ts @@ -44,7 +44,8 @@ export async function writeWorkflowGroupState( ): Promise<'wrote' | 'skipped'> { const { tableId, rowId, workspaceId, groupId, executionId } = ctx const requestId = ctx.requestId ?? `wfgrp-${executionId}` - const { getTableById, getRowById, updateRow } = await import('@/lib/table/service') + const { getTableById } = await import('@/lib/table/service') + const { getRowById, updateRow } = await import('@/lib/table/rows/service') const table = await getTableById(tableId) if (!table) { diff --git a/apps/sim/lib/table/column-keys.ts b/apps/sim/lib/table/column-keys.ts index 4e54d324656..ec770b047ea 100644 --- a/apps/sim/lib/table/column-keys.ts +++ b/apps/sim/lib/table/column-keys.ts @@ -9,7 +9,14 @@ */ import { generateId } from '@sim/utils/id' -import type { ColumnDefinition, Filter, RowData, Sort, TableSchema, WorkflowGroup } from './types' +import type { + ColumnDefinition, + Filter, + RowData, + Sort, + TableSchema, + WorkflowGroup, +} from '@/lib/table/types' /** * Resolves a column's stable storage key. Falls back to `name` for legacy diff --git a/apps/sim/lib/table/column-naming.ts b/apps/sim/lib/table/column-naming.ts index 9d993c346a1..1124e8da2df 100644 --- a/apps/sim/lib/table/column-naming.ts +++ b/apps/sim/lib/table/column-naming.ts @@ -6,7 +6,7 @@ * get from the sidebar. */ -import type { ColumnDefinition } from './types' +import type { ColumnDefinition } from '@/lib/table/types' /** * Slugifies a string into a `NAME_PATTERN`-safe column name. Lowercase, diff --git a/apps/sim/lib/table/columns/service.ts b/apps/sim/lib/table/columns/service.ts new file mode 100644 index 00000000000..4eafabd456d --- /dev/null +++ b/apps/sim/lib/table/columns/service.ts @@ -0,0 +1,668 @@ +/** + * Column and schema-management service for user tables. + * + * Standalone column-mutation operations (add, rename, delete, type change, + * constraint change) extracted from the table service. Each acquires the + * table's advisory lock via {@link withLockedTable} from `@/lib/table/service`. + * + * Use this for: workflow executor, background jobs, testing business logic. + * Use API routes for: HTTP requests, frontend clients. + */ + +import { db } from '@sim/db' +import { userTableDefinitions, userTableRows } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, count, eq, sql } from 'drizzle-orm' +import { columnMatchesRef, generateColumnId, getColumnId } from '@/lib/table/column-keys' +import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' +import { stripGroupExecutions } from '@/lib/table/rows/executions' +import { withLockedTable } from '@/lib/table/service' +import { scaledStatementTimeoutMs, setTableTxTimeouts } from '@/lib/table/tx' +import type { + DeleteColumnData, + RenameColumnData, + RowData, + TableDefinition, + TableMetadata, + TableSchema, + UpdateColumnConstraintsData, + UpdateColumnTypeData, +} from '@/lib/table/types' +import { assertValidSchema, stripGroupDeps } from '@/lib/table/workflow-columns' + +const logger = createLogger('TableColumnService') + +/** + * Adds a column to an existing table's schema. + * + * @param tableId - Table ID to update + * @param column - Column definition to add + * @param requestId - Request ID for logging + * @returns Updated table definition + * @throws Error if table not found or column name already exists + */ +export async function addTableColumn( + tableId: string, + column: { + id?: string + name: string + type: string + required?: boolean + unique?: boolean + position?: number + }, + requestId: string +): Promise { + return withLockedTable(tableId, async (table, trx) => { + if (!NAME_PATTERN.test(column.name)) { + throw new Error( + `Invalid column name "${column.name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.` + ) + } + + if (column.name.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { + throw new Error( + `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` + ) + } + + if (!COLUMN_TYPES.includes(column.type as (typeof COLUMN_TYPES)[number])) { + throw new Error( + `Invalid column type "${column.type}". Must be one of: ${COLUMN_TYPES.join(', ')}` + ) + } + + const schema = table.schema + if (schema.columns.some((c) => c.name.toLowerCase() === column.name.toLowerCase())) { + throw new Error(`Column "${column.name}" already exists`) + } + + if (schema.columns.length >= TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { + throw new Error( + `Table has reached maximum column limit (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE})` + ) + } + + const newColumn: TableSchema['columns'][number] = { + // Honor a caller-provided id (undo of a delete reuses the original id); + // otherwise mint a fresh one. + id: column.id ?? generateColumnId(), + name: column.name, + type: column.type as TableSchema['columns'][number]['type'], + required: column.required ?? false, + unique: column.unique ?? false, + } + const newColumnId = getColumnId(newColumn) + + const columns = [...schema.columns] + if (column.position !== undefined && column.position >= 0 && column.position < columns.length) { + columns.splice(column.position, 0, newColumn) + } else { + columns.push(newColumn) + } + + const updatedSchema: TableSchema = { ...schema, columns } + + // Keep `metadata.columnOrder` (a list of column ids) in sync: splicing the + // new column's id at the same index we used in `columns` keeps display + // ordering aligned with the user's intent for `position`-based inserts. + const existingOrder = table.metadata?.columnOrder + let updatedMetadata = table.metadata + if (existingOrder && existingOrder.length > 0 && !existingOrder.includes(newColumnId)) { + let insertIdx = existingOrder.length + if (column.position !== undefined && column.position >= 0) { + // Anchor on the column previously at `position` — that column shifted + // right by one in `columns`, so the new id slots in at its old spot. + const anchor = schema.columns[column.position] + if (anchor) { + const anchorIdx = existingOrder.indexOf(getColumnId(anchor)) + if (anchorIdx !== -1) insertIdx = anchorIdx + } + } + const nextOrder = [...existingOrder] + nextOrder.splice(insertIdx, 0, newColumnId) + updatedMetadata = { ...table.metadata, columnOrder: nextOrder } + } + + assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) + + const now = new Date() + + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, tableId)) + + logger.info(`[${requestId}] Added column "${column.name}" to table ${tableId}`) + + return { + ...table, + schema: updatedSchema, + metadata: updatedMetadata, + updatedAt: now, + } + }) +} + +/** + * Renames a column in a table's schema and updates all row data keys. + * + * @param data - Rename column data + * @param requestId - Request ID for logging + * @returns Updated table definition + * @throws Error if table not found, column not found, or new name conflicts + */ +export async function renameColumn( + data: RenameColumnData, + requestId: string +): Promise { + return withLockedTable(data.tableId, async (table, trx) => { + if (!NAME_PATTERN.test(data.newName)) { + throw new Error( + `Invalid column name "${data.newName}". Column names must start with a letter or underscore, followed by alphanumeric characters or underscores.` + ) + } + + if (data.newName.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { + throw new Error( + `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` + ) + } + + const schema = table.schema + const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.oldName)) + if (columnIndex === -1) { + throw new Error(`Column "${data.oldName}" not found`) + } + + if ( + schema.columns.some( + (c, i) => i !== columnIndex && c.name.toLowerCase() === data.newName.toLowerCase() + ) + ) { + throw new Error(`Column "${data.newName}" already exists`) + } + + const targetColumn = schema.columns[columnIndex] + const actualOldName = targetColumn.name + + // Rename is metadata-only: stored rows, metadata, and workflow-group refs all + // key on the column's stable id, which a rename never changes — so this is a + // pure schema write, no per-row JSONB rewrite or group/metadata cascade. + // Stamp the current storage key as the id (for any not-yet-backfilled column) + // so existing rows stay reachable as the display name changes. + const columnId = targetColumn.id ?? actualOldName + const updatedColumns = schema.columns.map((c, i) => + i === columnIndex ? { ...c, id: columnId, name: data.newName } : c + ) + const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } + assertValidSchema(updatedSchema, table.metadata?.columnOrder) + + const now = new Date() + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + logger.info( + `[${requestId}] Renamed column "${actualOldName}" to "${data.newName}" in table ${data.tableId}` + ) + return { ...table, schema: updatedSchema, updatedAt: now } + }) +} + +/** Removes the given column-id keys from a metadata blob (widths/order/pinned). */ +function stripColumnIdsFromMetadata( + metadata: TableMetadata | null, + ids: ReadonlySet +): TableMetadata | null { + if (!metadata) return metadata + let next = metadata + if (metadata.columnWidths) { + const widths = { ...metadata.columnWidths } + let changed = false + for (const id of ids) + if (id in widths) { + delete widths[id] + changed = true + } + if (changed) next = { ...next, columnWidths: widths } + } + if (metadata.columnOrder?.some((id) => ids.has(id))) { + next = { ...next, columnOrder: metadata.columnOrder.filter((id) => !ids.has(id)) } + } + if (metadata.pinnedColumns?.some((id) => ids.has(id))) { + next = { ...next, pinnedColumns: metadata.pinnedColumns.filter((id) => !ids.has(id)) } + } + return next +} + +/** + * Fire-and-forget reclamation of a deleted column's row storage. The column is + * already gone from the schema, so reads never surface the orphaned id — + * dropping the JSONB key just frees space. Runs in its own transaction with a + * row-count-scaled timeout; failures are logged, not propagated. + */ +function stripColumnDataInBackground( + tableId: string, + columnIds: string[], + rowCount: number, + requestId: string +): void { + if (columnIds.length === 0) return + void (async () => { + try { + await db.transaction(async (trx) => { + const statementMs = scaledStatementTimeoutMs(rowCount, { + baseMs: 60_000, + perRowMs: 2 * columnIds.length, + }) + await setTableTxTimeouts(trx, { statementMs }) + for (const id of columnIds) { + await trx.execute( + sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${tableId} AND data ? ${id}::text` + ) + } + }) + logger.info( + `[${requestId}] Background-stripped deleted column data [${columnIds.join(', ')}] from table ${tableId}` + ) + } catch (err) { + logger.error( + `[${requestId}] Background column-data strip failed for table ${tableId} [${columnIds.join(', ')}]:`, + err + ) + } + })() +} + +/** + * Deletes a column from a table's schema. When id-keyed, returns once the schema + * is updated and reclaims the column's row-data storage in the background + * (fire-and-forget); the legacy path strips the row key synchronously. + * + * @param data - Delete column data + * @param requestId - Request ID for logging + * @returns Updated table definition + * @throws Error if table not found, column not found, or it's the last column + */ +export async function deleteColumn( + data: DeleteColumnData, + requestId: string +): Promise { + const { def, stripKey } = await withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) + if (columnIndex === -1) { + throw new Error(`Column "${data.columnName}" not found`) + } + + if (schema.columns.length <= 1) { + throw new Error('Cannot delete the last column in a table') + } + + const targetColumn = schema.columns[columnIndex] + const actualName = targetColumn.name + const columnId = getColumnId(targetColumn) + const ownerGroupId = targetColumn.workflowGroupId + + // Drop this column's reference (by id) from every group's outputs and + // `columns` dependency. If the column is the last output of its parent + // group, the group itself is also removed (a group with zero outputs is + // invalid). + let groupRemovedId: string | null = null + const updatedGroups = (schema.workflowGroups ?? []) + .map((group) => { + let next = group + if (ownerGroupId && group.id === ownerGroupId) { + const remaining = group.outputs.filter((o) => o.columnName !== columnId) + if (remaining.length === 0) { + groupRemovedId = group.id + } + next = { ...next, outputs: remaining } + } + return stripGroupDeps(next, new Set([columnId])) + }) + .filter((g) => g.id !== groupRemovedId) + + const updatedSchema: TableSchema = { + ...schema, + columns: schema.columns.filter((_, i) => i !== columnIndex), + ...(updatedGroups.length > 0 ? { workflowGroups: updatedGroups } : {}), + } + const updatedMetadata = stripColumnIdsFromMetadata( + table.metadata as TableMetadata | null, + new Set([columnId]) + ) + assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) + + const now = new Date() + + // Schema/metadata update commits now; the column's row-data storage is + // reclaimed in the background (fire-and-forget) — reads never surface the + // orphaned id since the column is already gone from the schema. + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + if (groupRemovedId) await stripGroupExecutions(trx, data.tableId, [groupRemovedId]) + + logger.info(`[${requestId}] Deleted column "${actualName}" from table ${data.tableId}`) + + return { + def: { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }, + stripKey: columnId, + } + }) + + stripColumnDataInBackground(data.tableId, [stripKey], def.rowCount ?? 0, requestId) + return def +} + +/** + * Deletes multiple columns from a table in a single transaction. + * Avoids the race condition of calling deleteColumn multiple times in parallel. + */ +export async function deleteColumns( + data: { tableId: string; columnNames: string[] }, + requestId: string +): Promise { + const { def, stripKeys } = await withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const namesToDelete = new Set() + const idsToDelete = new Set() + const notFound: string[] = [] + + for (const name of data.columnNames) { + const col = schema.columns.find((c) => columnMatchesRef(c, name)) + if (!col) { + notFound.push(name) + } else { + namesToDelete.add(col.name) + idsToDelete.add(getColumnId(col)) + } + } + + if (notFound.length > 0) { + throw new Error(`Columns not found: ${notFound.join(', ')}`) + } + + const remaining = schema.columns.filter((c) => !namesToDelete.has(c.name)) + if (remaining.length === 0) { + throw new Error('Cannot delete all columns from a table') + } + + // For each group, drop outputs whose column (by id) is being deleted. Groups + // that end up with zero outputs are removed entirely (they'd be invalid). + // Then any remaining group's dependencies referencing a removed column are + // cleaned up. + const removedGroupIds = new Set() + let updatedGroups = (schema.workflowGroups ?? []).map((group) => { + const remainingOutputs = group.outputs.filter((o) => !idsToDelete.has(o.columnName)) + if (remainingOutputs.length === 0) { + removedGroupIds.add(group.id) + } + return remainingOutputs.length === group.outputs.length + ? group + : { ...group, outputs: remainingOutputs } + }) + updatedGroups = updatedGroups + .filter((g) => !removedGroupIds.has(g.id)) + .map((group) => stripGroupDeps(group, idsToDelete)) + const updatedSchema: TableSchema = { + ...schema, + columns: remaining, + ...(updatedGroups.length > 0 ? { workflowGroups: updatedGroups } : {}), + } + const updatedMetadata = stripColumnIdsFromMetadata( + table.metadata as TableMetadata | null, + idsToDelete + ) + assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) + + const now = new Date() + + // Schema/metadata commit now; row storage for the deleted columns is + // reclaimed in the background (fire-and-forget). + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + await stripGroupExecutions(trx, data.tableId, removedGroupIds) + + logger.info( + `[${requestId}] Deleted columns [${[...namesToDelete].join(', ')}] from table ${data.tableId}` + ) + + return { + def: { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }, + stripKeys: Array.from(idsToDelete), + } + }) + + if (stripKeys.length > 0) { + stripColumnDataInBackground(data.tableId, stripKeys, def.rowCount ?? 0, requestId) + } + return def +} + +/** + * Changes the type of a column. Validates that existing data is compatible. + * + * @param data - Update column type data + * @param requestId - Request ID for logging + * @returns Updated table definition + * @throws Error if table not found, column not found, or existing data is incompatible + */ +export async function updateColumnType( + data: UpdateColumnTypeData, + requestId: string +): Promise { + return withLockedTable(data.tableId, async (table, trx) => { + // Scale both statement and idle timeouts to row count: the compatibility + // check below iterates every row in Node between the row SELECT and the + // schema UPDATE, leaving the transaction idle for that gap. The default 5s + // `idle_in_transaction_session_timeout` would abort a valid type change on + // a large table. + const timeoutMs = scaledStatementTimeoutMs(table.rowCount ?? 0, { + baseMs: 60_000, + perRowMs: 2, + }) + await setTableTxTimeouts(trx, { statementMs: timeoutMs, idleMs: timeoutMs }) + + if (!(COLUMN_TYPES as readonly string[]).includes(data.newType)) { + throw new Error( + `Invalid column type "${data.newType}". Valid types: ${COLUMN_TYPES.join(', ')}` + ) + } + + const schema = table.schema + const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) + if (columnIndex === -1) { + throw new Error(`Column "${data.columnName}" not found`) + } + + const column = schema.columns[columnIndex] + if (column.type === data.newType) { + return table + } + const columnKey = getColumnId(column) + + // Validate existing data is compatible with the new type + const rows = await trx + .select({ id: userTableRows.id, data: userTableRows.data }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, data.tableId), + sql`${userTableRows.data} ? ${columnKey}`, + sql`${userTableRows.data}->>${columnKey}::text IS NOT NULL` + ) + ) + + let incompatibleCount = 0 + for (const row of rows) { + const rowData = row.data as RowData + const value = rowData[columnKey] + if (value === null || value === undefined) continue + + if (!isValueCompatibleWithType(value, data.newType)) { + incompatibleCount++ + } + } + + if (incompatibleCount > 0) { + throw new Error( + `Cannot change column "${column.name}" to type "${data.newType}": ${incompatibleCount} row(s) have incompatible values. Fix or remove the incompatible values first.` + ) + } + + const updatedColumns = schema.columns.map((c, i) => + i === columnIndex ? { ...c, type: data.newType } : c + ) + const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } + const now = new Date() + + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + logger.info( + `[${requestId}] Changed column "${column.name}" type from "${column.type}" to "${data.newType}" in table ${data.tableId}` + ) + + return { ...table, schema: updatedSchema, updatedAt: now } + }) +} + +/** + * Updates constraints (required, unique) on a column. + * + * @param data - Update column constraints data + * @param requestId - Request ID for logging + * @returns Updated table definition + * @throws Error if table not found, column not found, or existing data violates the constraint + */ +export async function updateColumnConstraints( + data: UpdateColumnConstraintsData, + requestId: string +): Promise { + return withLockedTable(data.tableId, async (table, trx) => { + // Scale both statement and idle timeouts to row count: the required/unique + // validation runs between separate queries inside this transaction, leaving + // it briefly idle. Match `updateColumnType` so the default 5s + // `idle_in_transaction_session_timeout` can't abort a valid change on a + // large table. + const timeoutMs = scaledStatementTimeoutMs(table.rowCount ?? 0, { + baseMs: 60_000, + perRowMs: 2, + }) + await setTableTxTimeouts(trx, { statementMs: timeoutMs, idleMs: timeoutMs }) + + const schema = table.schema + const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) + if (columnIndex === -1) { + throw new Error(`Column "${data.columnName}" not found`) + } + + const column = schema.columns[columnIndex] + const columnKey = getColumnId(column) + if (column.workflowGroupId) { + throw new Error( + `Cannot change constraints on workflow-output column "${column.name}". Constraints aren't applicable to columns whose values come from workflow execution.` + ) + } + if (data.required === true && !column.required) { + const [result] = await trx + .select({ count: count() }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, data.tableId), + sql`(NOT (${userTableRows.data} ? ${columnKey}) OR ${userTableRows.data}->>${columnKey}::text IS NULL)` + ) + ) + + if (result.count > 0) { + throw new Error( + `Cannot set column "${column.name}" as required: ${result.count} row(s) have null or missing values` + ) + } + } + + if (data.unique === true && !column.unique) { + const duplicates = (await trx.execute( + sql`SELECT ${userTableRows.data}->>${columnKey}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${data.tableId} AND ${userTableRows.data} ? ${columnKey} AND ${userTableRows.data}->>${columnKey}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1` + )) as { val: string; cnt: number }[] + + if (duplicates.length > 0) { + throw new Error(`Cannot set column "${column.name}" as unique: duplicate values exist`) + } + } + + const updatedColumns = schema.columns.map((c, i) => + i === columnIndex + ? { + ...c, + ...(data.required !== undefined ? { required: data.required } : {}), + ...(data.unique !== undefined ? { unique: data.unique } : {}), + } + : c + ) + const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } + const now = new Date() + + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + logger.info( + `[${requestId}] Updated constraints for column "${column.name}" in table ${data.tableId}` + ) + + return { ...table, schema: updatedSchema, updatedAt: now } + }) +} + +/** + * Checks if a value is compatible with a target column type. + */ +function isValueCompatibleWithType( + value: unknown, + targetType: (typeof COLUMN_TYPES)[number] +): boolean { + if (value === null || value === undefined) return true + + switch (targetType) { + case 'string': + return true + case 'number': { + if (typeof value === 'number') return Number.isFinite(value) + if (typeof value === 'string') { + const num = Number(value) + return Number.isFinite(num) && value.trim() !== '' + } + return false + } + case 'boolean': { + if (typeof value === 'boolean') return true + if (typeof value === 'string') + return ['true', 'false', '1', '0'].includes(value.toLowerCase()) + if (typeof value === 'number') return value === 0 || value === 1 + return false + } + case 'date': { + if (value instanceof Date) return !Number.isNaN(value.getTime()) + if (typeof value === 'string') return !Number.isNaN(Date.parse(value)) + return false + } + case 'json': + return true + default: + return false + } +} diff --git a/apps/sim/lib/table/delete-runner.test.ts b/apps/sim/lib/table/delete-runner.test.ts index 1259e76aa85..d06285d51a7 100644 --- a/apps/sim/lib/table/delete-runner.test.ts +++ b/apps/sim/lib/table/delete-runner.test.ts @@ -27,13 +27,17 @@ const { vi.mock('@/lib/table/service', () => ({ getTableById: mockGetTableById, +})) +vi.mock('@/lib/table/jobs/service', () => ({ getJobProgress: mockGetJobProgress, - selectRowIdPage: mockSelectRowIdPage, - deletePageByIds: mockDeletePageByIds, updateJobProgress: mockUpdateJobProgress, markJobReady: mockMarkJobReady, markJobFailed: mockMarkJobFailed, })) +vi.mock('@/lib/table/rows/ordering', () => ({ + selectRowIdPage: mockSelectRowIdPage, + deletePageByIds: mockDeletePageByIds, +})) vi.mock('@/lib/table/events', () => ({ appendTableEvent: mockAppendTableEvent })) vi.mock('@/lib/table/sql', () => ({ buildFilterClause: mockBuildFilterClause })) vi.mock('@/lib/table/constants', () => ({ diff --git a/apps/sim/lib/table/delete-runner.ts b/apps/sim/lib/table/delete-runner.ts index 0065b60ba5a..5fb4a6e3708 100644 --- a/apps/sim/lib/table/delete-runner.ts +++ b/apps/sim/lib/table/delete-runner.ts @@ -6,14 +6,13 @@ import type { Filter } from '@/lib/table' import { TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' import { appendTableEvent } from '@/lib/table/events' import { - deletePageByIds, getJobProgress, - getTableById, markJobFailed, markJobReady, - selectRowIdPage, updateJobProgress, -} from '@/lib/table/service' +} from '@/lib/table/jobs/service' +import { deletePageByIds, selectRowIdPage } from '@/lib/table/rows/ordering' +import { getTableById } from '@/lib/table/service' import { buildFilterClause } from '@/lib/table/sql' const logger = createLogger('TableDeleteRunner') diff --git a/apps/sim/lib/table/deps.ts b/apps/sim/lib/table/deps.ts index a8e2e246fd2..55a5db6b9a1 100644 --- a/apps/sim/lib/table/deps.ts +++ b/apps/sim/lib/table/deps.ts @@ -5,7 +5,13 @@ */ import { createLogger } from '@sim/logger' -import type { RowData, RowExecutionMetadata, RowExecutions, TableRow, WorkflowGroup } from './types' +import type { + RowData, + RowExecutionMetadata, + RowExecutions, + TableRow, + WorkflowGroup, +} from '@/lib/table/types' const logger = createLogger('OptimisticCascade') diff --git a/apps/sim/lib/table/dispatcher.ts b/apps/sim/lib/table/dispatcher.ts index 99593f4097c..449bc531914 100644 --- a/apps/sim/lib/table/dispatcher.ts +++ b/apps/sim/lib/table/dispatcher.ts @@ -30,7 +30,7 @@ import { TABLE_CONCURRENCY_LIMIT, toTableRow, type WorkflowGroupCellPayload, -} from './workflow-columns' +} from '@/lib/table/workflow-columns' const logger = createLogger('TableRunDispatcher') @@ -375,7 +375,7 @@ export async function dispatcherStep(dispatchId: string): Promise ({ getTableById: mockGetTableById, +})) +vi.mock('@/lib/table/jobs/service', () => ({ selectExportRowPage: mockSelectExportRowPage, updateJobProgress: mockUpdateJobProgress, markJobReady: mockMarkJobReady, diff --git a/apps/sim/lib/table/export-runner.ts b/apps/sim/lib/table/export-runner.ts index f2df9d4e1c2..e6f6f834f1f 100644 --- a/apps/sim/lib/table/export-runner.ts +++ b/apps/sim/lib/table/export-runner.ts @@ -10,13 +10,13 @@ import { toCsvRow, } from '@/lib/table/export-format' import { - getTableById, markJobFailed, markJobReady, selectExportRowPage, setJobResultKey, updateJobProgress, -} from '@/lib/table/service' +} from '@/lib/table/jobs/service' +import { getTableById } from '@/lib/table/service' import { deleteFile, uploadFile } from '@/lib/uploads/core/storage-service' const logger = createLogger('TableExportRunner') diff --git a/apps/sim/lib/table/hooks/index.ts b/apps/sim/lib/table/hooks/index.ts index ffa84c9ced3..9597c85c51f 100644 --- a/apps/sim/lib/table/hooks/index.ts +++ b/apps/sim/lib/table/hooks/index.ts @@ -1 +1 @@ -export * from './use-table-columns' +export * from '@/lib/table/hooks/use-table-columns' diff --git a/apps/sim/lib/table/hooks/use-table-columns.ts b/apps/sim/lib/table/hooks/use-table-columns.ts index 3501a4ee35a..4d882f66587 100644 --- a/apps/sim/lib/table/hooks/use-table-columns.ts +++ b/apps/sim/lib/table/hooks/use-table-columns.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' +import type { ColumnOption } from '@/lib/table/types' import { useTable } from '@/hooks/queries/tables' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import type { ColumnOption } from '../types' interface UseTableColumnsOptions { tableId: string | null | undefined diff --git a/apps/sim/lib/table/import-data.ts b/apps/sim/lib/table/import-data.ts new file mode 100644 index 00000000000..bf25f2e15ca --- /dev/null +++ b/apps/sim/lib/table/import-data.ts @@ -0,0 +1,200 @@ +/** + * Import-job table-data write operations — bulk insert, schema setup, and + * append/replace used by `import-runner.ts` and the import route. Distinct from + * `import.ts` (CSV parsing) and `import-runner.ts` (the job runner). + */ + +import { db } from '@sim/db' +import { userTableDefinitions, userTableRows } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { eq } from 'drizzle-orm' +import { CSV_MAX_BATCH_SIZE } from '@/lib/table/import' +import { nKeysBetween } from '@/lib/table/order-key' +import { acquireRowOrderLock } from '@/lib/table/rows/ordering' +import { batchInsertRowsWithTx, replaceTableRowsWithTx } from '@/lib/table/rows/service' +import { addTableColumnsWithTx } from '@/lib/table/service' +import type { + ReplaceRowsResult, + RowData, + TableDefinition, + TableRow, + TableSchema, +} from '@/lib/table/types' +import { + checkBatchUniqueConstraintsDb, + coerceRowToSchema, + getUniqueColumns, + validateRowSize, +} from '@/lib/table/validation' + +const logger = createLogger('TableImportData') + +/** One batch of rows for a background import (see {@link bulkInsertImportBatch}). */ +export interface BulkImportBatch { + tableId: string + workspaceId: string + userId?: string + rows: RowData[] + /** Position of the first row in this batch; rows get contiguous positions from here. */ + startPosition: number + /** Previous batch's last `order_key` (the append anchor); null for the first batch / empty table. */ + afterOrderKey?: string | null +} + +/** + * Inserts one batch of rows for an async import in a single committed statement. + * + * Differs from {@link batchInsertRowsWithTx} for the bulk-load case: caller-supplied + * contiguous positions (no `acquireTablePositionLock` / `nextAutoPosition` scan — an + * import owns its hidden table as the sole writer), no `RETURNING`, and **no + * `fireTableTrigger` / `runWorkflowColumn`** (a 1M-row import must not dispatch a + * workflow run per row). `row_count` is maintained set-based by the statement-level + * trigger. There is no surrounding transaction and no rollback: each batch commits on + * its own, so committed batches persist even if a later batch fails. + * + * Throws on row-size/schema/unique violations or if the statement-level trigger rejects + * the batch for crossing `max_rows`; the caller marks the import failed. + */ +export async function bulkInsertImportBatch( + data: BulkImportBatch, + table: TableDefinition, + requestId: string +): Promise<{ inserted: number; lastOrderKey: string | null }> { + for (let i = 0; i < data.rows.length; i++) { + const sizeValidation = validateRowSize(data.rows[i]) + if (!sizeValidation.valid) { + throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) + } + const schemaValidation = coerceRowToSchema(data.rows[i], table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) + } + } + + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0) { + const uniqueResult = await checkBatchUniqueConstraintsDb( + data.tableId, + data.rows, + table.schema, + db + ) + if (!uniqueResult.valid) { + throw new Error( + uniqueResult.errors.map((e) => `Row ${e.row + 1}: ${e.errors.join(', ')}`).join('; ') + ) + } + } + + const now = new Date() + // Import worker is the table's sole writer; append keys after the anchor the caller threads + // from the previous batch's last key — no per-batch max(order_key) scan over a growing table. + const orderKeys = nKeysBetween(data.afterOrderKey ?? null, null, data.rows.length) + const rowsToInsert = data.rows.map((rowData, i) => ({ + id: `row_${generateId().replace(/-/g, '')}`, + tableId: data.tableId, + workspaceId: data.workspaceId, + data: rowData, + position: data.startPosition + i, + orderKey: orderKeys[i], + createdAt: now, + updatedAt: now, + ...(data.userId ? { createdBy: data.userId } : {}), + })) + + await db.insert(userTableRows).values(rowsToInsert) + logger.info(`[${requestId}] Bulk-imported ${rowsToInsert.length} rows into table ${data.tableId}`) + return { + inserted: rowsToInsert.length, + lastOrderKey: orderKeys[orderKeys.length - 1] ?? data.afterOrderKey ?? null, + } +} + +/** Deletes every row of a table (set-based; the statement-level trigger zeroes `row_count`). */ +export async function deleteAllTableRows(tableId: string): Promise { + await db.delete(userTableRows).where(eq(userTableRows.tableId, tableId)) +} + +/** + * Adds columns to a table during an import (the `createColumns` flow), wrapping the + * tx-bound {@link addTableColumnsWithTx} in its own transaction. Returns the updated table. + */ +export async function addImportColumns( + table: TableDefinition, + additions: { name: string; type: string }[], + requestId: string +): Promise { + return db.transaction((trx) => addTableColumnsWithTx(trx, table, additions, requestId)) +} + +/** Overwrites a table's schema during an import (used when inferring columns from the file). */ +export async function setTableSchemaForImport(tableId: string, schema: TableSchema): Promise { + await db + .update(userTableDefinitions) + .set({ schema, updatedAt: new Date() }) + .where(eq(userTableDefinitions.id, tableId)) +} + +/** + * Owns the append-import transaction so the API route never holds a `trx`: + * optionally creates the new columns, then inserts every row in CSV-sized + * batches — all atomic. Caller fires {@link dispatchAfterBatchInsert} after this + * resolves (post-commit), mirroring the other batch-insert sites. + */ +export async function importAppendRows( + table: TableDefinition, + additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], + rows: RowData[], + ctx: { workspaceId: string; userId?: string; requestId: string } +): Promise<{ inserted: TableRow[]; table: TableDefinition }> { + return db.transaction(async (trx) => { + let working = table + if (additions.length > 0) { + // Take the row-order lock before creating columns so this path uses the + // same rows_pos → user_table_definitions order as plain inserts. Creating + // columns first would lock the definition row before rows_pos, inverting + // the order and deadlocking concurrent inserts on this table. The lock is + // re-entrant, so the per-batch acquire below is a no-op. + await acquireRowOrderLock(trx, table.id) + working = await addTableColumnsWithTx(trx, table, additions, ctx.requestId) + } + const inserted: TableRow[] = [] + for (let i = 0; i < rows.length; i += CSV_MAX_BATCH_SIZE) { + const batch = rows.slice(i, i + CSV_MAX_BATCH_SIZE) + const batchInserted = await batchInsertRowsWithTx( + trx, + { tableId: working.id, rows: batch, workspaceId: ctx.workspaceId, userId: ctx.userId }, + working, + generateId().slice(0, 8) + ) + inserted.push(...batchInserted) + } + return { inserted, table: working } + }) +} + +/** + * Owns the replace-import transaction: optionally creates the new columns, then + * replaces all rows — atomically. Keeps `trx` out of the API route. + */ +export async function importReplaceRows( + table: TableDefinition, + additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], + data: { rows: RowData[]; workspaceId: string; userId?: string }, + requestId: string +): Promise { + return db.transaction(async (trx) => { + let working = table + if (additions.length > 0) { + await acquireRowOrderLock(trx, table.id) + working = await addTableColumnsWithTx(trx, table, additions, requestId) + } + return replaceTableRowsWithTx( + trx, + { tableId: working.id, rows: data.rows, workspaceId: data.workspaceId, userId: data.userId }, + working, + requestId + ) + }) +} diff --git a/apps/sim/lib/table/import-runner.ts b/apps/sim/lib/table/import-runner.ts index 44a7d3ec693..5391d22a238 100644 --- a/apps/sim/lib/table/import-runner.ts +++ b/apps/sim/lib/table/import-runner.ts @@ -23,14 +23,11 @@ import { addImportColumns, bulkInsertImportBatch, deleteAllTableRows, - getTableById, - markJobFailed, - markJobReady, - nextImportStartOrderKey, - nextImportStartPosition, setTableSchemaForImport, - updateJobProgress, -} from '@/lib/table/service' +} from '@/lib/table/import-data' +import { markJobFailed, markJobReady, updateJobProgress } from '@/lib/table/jobs/service' +import { nextImportStartOrderKey, nextImportStartPosition } from '@/lib/table/rows/ordering' +import { getTableById } from '@/lib/table/service' import { deleteFile, downloadFileStream, headObject } from '@/lib/uploads/core/storage-service' import { normalizeColumn } from '@/app/api/table/utils' diff --git a/apps/sim/lib/table/index.ts b/apps/sim/lib/table/index.ts index 32f487cb9dd..c79a13f182c 100644 --- a/apps/sim/lib/table/index.ts +++ b/apps/sim/lib/table/index.ts @@ -5,13 +5,18 @@ * Import hooks directly from '@/lib/table/hooks' in client components. */ -export * from './billing' -export * from './column-keys' -export * from './constants' -export * from './import' -export * from './llm' -export * from './query-builder' -export * from './service' -export * from './sql' -export * from './types' -export * from './validation' +export * from '@/lib/table/billing' +export * from '@/lib/table/column-keys' +export * from '@/lib/table/columns/service' +export * from '@/lib/table/constants' +export * from '@/lib/table/import' +export * from '@/lib/table/import-data' +export * from '@/lib/table/jobs/service' +export * from '@/lib/table/llm' +export * from '@/lib/table/query-builder' +export * from '@/lib/table/rows/service' +export * from '@/lib/table/service' +export * from '@/lib/table/sql' +export * from '@/lib/table/types' +export * from '@/lib/table/validation' +export * from '@/lib/table/workflow-groups/service' diff --git a/apps/sim/lib/table/jobs/service.ts b/apps/sim/lib/table/jobs/service.ts new file mode 100644 index 00000000000..124f3d95c04 --- /dev/null +++ b/apps/sim/lib/table/jobs/service.ts @@ -0,0 +1,383 @@ +/** + * Table background-job service for user tables. + * + * The `table_jobs` state machine (claim / progress / terminal transitions), the + * latest-job reads that enrich a {@link TableDefinition}, and the export-job read + * paths — extracted from the table service. Operates purely on the `table_jobs` + * table (plus `selectExportRowPage`, which pages rows through the shared + * `pendingDeleteMask`), so it never imports the table-root service. + * + * Use this for: workflow executor, background jobs, testing business logic. + * Use API routes for: HTTP requests, frontend clients. + */ + +import { db } from '@sim/db' +import { tableJobs, userTableDefinitions, userTableRows } from '@sim/db/schema' +import { and, asc, desc, eq, gt, inArray, ne, or, sql } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { pendingDeleteMask } from '@/lib/table/rows/service' +import type { + RowData, + TableDefinition, + TableDeleteJobPayload, + TableExportJobPayload, + TableJobType, +} from '@/lib/table/types' + +/** Job fields projected onto a {@link TableDefinition}, derived from its latest `table_jobs` row. */ +interface DerivedJobFields { + jobStatus: TableDefinition['jobStatus'] + jobId: string | null + jobType: TableDefinition['jobType'] + jobError: string | null + jobRowsProcessed: number + /** + * Rows a running delete job still has to remove (its doomed estimate minus + * deletions so far). Internal to count adjustment — callers subtract it from + * the raw `row_count` so list/detail counts match the read path's delete + * mask (a mid-delete refresh must not resurrect the count). Not on the wire. + */ + pendingDeleteRemaining: number +} + +export const EMPTY_JOB_FIELDS: DerivedJobFields = { + jobStatus: null, + jobId: null, + jobType: null, + jobError: null, + jobRowsProcessed: 0, + pendingDeleteRemaining: 0, +} + +function mapJobRow( + row: + | { + id: string + type: string + status: string + rowsProcessed: number + error: string | null + payload: unknown + } + | undefined +): DerivedJobFields { + if (!row) return EMPTY_JOB_FIELDS + const doomedCount = + row.type === 'delete' && row.status === 'running' + ? ((row.payload as TableDeleteJobPayload | null)?.doomedCount ?? 0) + : 0 + return { + jobStatus: row.status as TableDefinition['jobStatus'], + jobId: row.id, + jobType: row.type as TableDefinition['jobType'], + jobError: row.error, + jobRowsProcessed: row.rowsProcessed, + pendingDeleteRemaining: Math.max(0, doomedCount - row.rowsProcessed), + } +} + +const JOB_PROJECTION = { + id: tableJobs.id, + type: tableJobs.type, + status: tableJobs.status, + rowsProcessed: tableJobs.rowsProcessed, + error: tableJobs.error, + payload: tableJobs.payload, +} as const + +/** + * The latest job for one table (the running one if present, else the most recent terminal). + * Exports are excluded: they're read-only, run concurrently with other jobs, and have their own + * client surface — surfacing one here would clobber the import/delete/backfill status the tray + * and SSE consumer derive from these fields. + */ +export async function latestJobForTable( + tableId: string, + executor: DbOrTx = db +): Promise { + const [row] = await executor + .select(JOB_PROJECTION) + .from(tableJobs) + .where(and(eq(tableJobs.tableId, tableId), ne(tableJobs.type, 'export'))) + .orderBy(desc(tableJobs.startedAt)) + .limit(1) + return mapJobRow(row) +} + +/** Latest non-export job per table for a batch of ids, via `DISTINCT ON (table_id)`. */ +export async function latestJobsForTables( + tableIds: string[] +): Promise> { + const map = new Map() + if (tableIds.length === 0) return map + const rows = await db + .selectDistinctOn([tableJobs.tableId], { tableId: tableJobs.tableId, ...JOB_PROJECTION }) + .from(tableJobs) + .where(and(inArray(tableJobs.tableId, tableIds), ne(tableJobs.type, 'export'))) + .orderBy(tableJobs.tableId, desc(tableJobs.startedAt)) + for (const row of rows) map.set(row.tableId, mapJobRow(row)) + return map +} + +/** + * Atomically claims a table's single background-job slot by inserting a `running` row into + * `table_jobs`. The partial-unique index on `table_id WHERE status = 'running'` is the + * concurrency gate: a second insert while a job runs hits `ON CONFLICT DO NOTHING` and returns no + * row, so import and delete (and two imports) are mutually exclusive for free. Returns whether it + * claimed the slot; the caller returns 409 when it didn't. + */ +export async function markTableJobRunning( + tableId: string, + jobId: string, + type: TableJobType, + /** Type-specific scope persisted to `table_jobs.payload` (e.g. {@link TableDeleteJobPayload}) + * so read paths can mask the job's effect while it runs. */ + payload?: unknown +): Promise { + // workspace_id is immutable; the atomic gate is the INSERT's conflict, not this read. + const [def] = await db + .select({ workspaceId: userTableDefinitions.workspaceId }) + .from(userTableDefinitions) + .where(eq(userTableDefinitions.id, tableId)) + .limit(1) + if (!def) return false + const inserted = await db + .insert(tableJobs) + .values({ + id: jobId, + tableId, + workspaceId: def.workspaceId, + type, + status: 'running', + payload: payload ?? null, + }) + .onConflictDoNothing() + .returning({ id: tableJobs.id }) + return inserted.length > 0 +} + +/** + * Releases a claim taken by {@link markTableJobRunning} for a synchronous job — deletes the + * transient claim row. Scoped to `jobId` + still-running so it only clears its own claim, never a + * newer run. A sync route claims, writes, then releases here in a `finally`. + */ +export async function releaseJobClaim(tableId: string, jobId: string): Promise { + await db + .delete(tableJobs) + .where( + and(eq(tableJobs.id, jobId), eq(tableJobs.tableId, tableId), eq(tableJobs.status, 'running')) + ) +} + +/** + * Records job progress (rows processed so far) and bumps `updated_at` so the stale-job janitor + * (`cleanup-stale-executions`) sees a live heartbeat. + * + * Scoped to `jobId` AND `status = 'running'`: a stale/superseded worker no longer matches (its + * write is a no-op), and once the job is terminal (e.g. canceled) the match fails too — so this + * returning `false` is the worker's signal to stop. Returns whether this worker still owns an + * in-flight job. + */ +export async function updateJobProgress( + tableId: string, + rowsProcessed: number, + jobId: string +): Promise { + const updated = await db + .update(tableJobs) + .set({ rowsProcessed, updatedAt: new Date() }) + .where(ownsActiveJob(tableId, jobId)) + .returning({ id: tableJobs.id }) + return updated.length > 0 +} + +/** + * Reads the persisted progress of an in-flight job this worker still owns (`null` when the job + * was canceled/superseded). A retried run seeds its counter from this so progress stays + * cumulative — earlier attempts' batches are already committed, and restarting from zero would + * clobber `rows_processed` (and every count derived from it) with the retry's smaller number. + */ +export async function getJobProgress(tableId: string, jobId: string): Promise { + const [job] = await db + .select({ rowsProcessed: tableJobs.rowsProcessed }) + .from(tableJobs) + .where(ownsActiveJob(tableId, jobId)) + .limit(1) + return job ? job.rowsProcessed : null +} + +/** + * One keyset page of rows for the export worker, ordered by `(position, id)`. Keyset (not + * OFFSET) keeps each page O(page) — offset paging re-scans every prior row per page, which is + * O(N²) across a large export. `(position, id)` is total (position exists on every row; id breaks + * ties) and served by the `(table_id, position)` index; under fractional ordering a manually + * reordered table may export in near-grid rather than exact grid order — the right trade for a + * bulk dump. The delete-job visibility mask applies, like every user-facing read. + */ +export async function selectExportRowPage( + table: TableDefinition, + after: { position: number; id: string } | null, + limit: number +): Promise> { + const deleteMask = await pendingDeleteMask(table) + const rows = await db + .select({ id: userTableRows.id, data: userTableRows.data, position: userTableRows.position }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, table.id), + eq(userTableRows.workspaceId, table.workspaceId), + deleteMask, + after + ? sql`(${userTableRows.position}, ${userTableRows.id}) > (${after.position}, ${after.id})` + : undefined + ) + ) + .orderBy(asc(userTableRows.position), asc(userTableRows.id)) + .limit(limit) + return rows as Array<{ id: string; data: RowData; position: number }> +} + +/** How long a terminal export stays listable (and re-downloadable from the tray). */ +const EXPORT_JOB_VISIBILITY_MS = 10 * 60 * 1000 + +export interface WorkspaceExportJob { + jobId: string + tableId: string + tableName: string + status: string + rowsProcessed: number + format: 'csv' | 'json' + hasResult: boolean + error: string | null +} + +/** + * Export jobs the tray surfaces for a workspace: everything running, plus terminals from the last + * {@link EXPORT_JOB_VISIBILITY_MS} so a just-finished export stays re-downloadable. Exports live + * outside the table-level job derivation (which excludes them), so this is their read path. + */ +export async function listWorkspaceExportJobs(workspaceId: string): Promise { + const visibilityCutoff = new Date(Date.now() - EXPORT_JOB_VISIBILITY_MS) + const rows = await db + .select({ + jobId: tableJobs.id, + tableId: tableJobs.tableId, + tableName: userTableDefinitions.name, + status: tableJobs.status, + rowsProcessed: tableJobs.rowsProcessed, + payload: tableJobs.payload, + error: tableJobs.error, + }) + .from(tableJobs) + .innerJoin(userTableDefinitions, eq(userTableDefinitions.id, tableJobs.tableId)) + .where( + and( + eq(tableJobs.workspaceId, workspaceId), + eq(tableJobs.type, 'export'), + or(eq(tableJobs.status, 'running'), gt(tableJobs.updatedAt, visibilityCutoff)) + ) + ) + .orderBy(desc(tableJobs.startedAt)) + return rows.map((r) => { + const payload = r.payload as TableExportJobPayload | null + return { + jobId: r.jobId, + tableId: r.tableId, + tableName: r.tableName, + status: r.status, + rowsProcessed: r.rowsProcessed, + format: payload?.format ?? 'csv', + hasResult: Boolean(payload?.resultKey), + error: r.error, + } + }) +} + +/** Reads one job row (type/status/payload) scoped to its table. Null when absent. */ +export async function getTableJob( + tableId: string, + jobId: string +): Promise<{ id: string; type: string; status: string; payload: unknown } | null> { + const [job] = await db + .select({ + id: tableJobs.id, + type: tableJobs.type, + status: tableJobs.status, + payload: tableJobs.payload, + }) + .from(tableJobs) + .where(and(eq(tableJobs.id, jobId), eq(tableJobs.tableId, tableId))) + .limit(1) + return job ?? null +} + +/** + * Stamps an export job's generated-file storage key onto its payload (`{ resultKey }` merge). + * Scoped to the still-running job so a superseded attempt can't clobber a newer run's result. + * The download route reads it; the janitor deletes the file when the terminal job is pruned. + */ +export async function setJobResultKey( + tableId: string, + jobId: string, + resultKey: string +): Promise { + await db + .update(tableJobs) + .set({ + payload: sql`coalesce(${tableJobs.payload}, '{}'::jsonb) || jsonb_build_object('resultKey', ${resultKey}::text)`, + updatedAt: new Date(), + }) + .where(ownsActiveJob(tableId, jobId)) +} + +/** Shared WHERE for terminal transitions: this job run, and still in-flight (write-once). */ +function ownsActiveJob(tableId: string, jobId: string) { + return and( + eq(tableJobs.id, jobId), + eq(tableJobs.tableId, tableId), + eq(tableJobs.status, 'running') + ) +} + +/** + * Marks a job complete. No-op unless it's still this in-flight run. Returns whether it + * transitioned, so the worker only emits the `ready` event when it actually won (and not after a + * cancel / supersede). + */ +export async function markJobReady(tableId: string, jobId: string): Promise { + const now = new Date() + const updated = await db + .update(tableJobs) + .set({ status: 'ready', error: null, completedAt: now, updatedAt: now }) + .where(ownsActiveJob(tableId, jobId)) + .returning({ id: tableJobs.id }) + return updated.length > 0 +} + +/** + * Marks a job failed, leaving any already-committed work in place. No-op unless it's still this + * in-flight run (so a stale worker can't clobber a newer job or a cancel). + */ +export async function markJobFailed(tableId: string, jobId: string, error: string): Promise { + const now = new Date() + await db + .update(tableJobs) + .set({ status: 'failed', error: error.slice(0, 2000), completedAt: now, updatedAt: now }) + .where(ownsActiveJob(tableId, jobId)) +} + +/** + * Marks an in-flight job canceled (user-initiated). No-op unless it's still running. The + * worker's next ownership check then returns `false` and it stops; committed work is left in + * place (no rollback). Returns whether a running job was actually canceled. + */ +export async function markJobCanceled(tableId: string, jobId: string): Promise { + const now = new Date() + const updated = await db + .update(tableJobs) + .set({ status: 'canceled', completedAt: now, updatedAt: now }) + .where(ownsActiveJob(tableId, jobId)) + .returning({ id: tableJobs.id }) + return updated.length > 0 +} diff --git a/apps/sim/lib/table/llm/enrichment.ts b/apps/sim/lib/table/llm/enrichment.ts index 98e13bfd286..2d9cb7d5d67 100644 --- a/apps/sim/lib/table/llm/enrichment.ts +++ b/apps/sim/lib/table/llm/enrichment.ts @@ -5,7 +5,7 @@ * with table-specific information so LLMs can construct proper queries. */ -import type { TableSummary } from '../types' +import type { TableSummary } from '@/lib/table/types' /** * Operations that use filters and need filter-specific enrichment. diff --git a/apps/sim/lib/table/llm/index.ts b/apps/sim/lib/table/llm/index.ts index 35e73a4299c..4ec60ea4baf 100644 --- a/apps/sim/lib/table/llm/index.ts +++ b/apps/sim/lib/table/llm/index.ts @@ -1 +1 @@ -export * from './enrichment' +export * from '@/lib/table/llm/enrichment' diff --git a/apps/sim/lib/table/llm/wand.ts b/apps/sim/lib/table/llm/wand.ts index 30c036c64ed..d90220c8073 100644 --- a/apps/sim/lib/table/llm/wand.ts +++ b/apps/sim/lib/table/llm/wand.ts @@ -6,7 +6,7 @@ import { db } from '@sim/db' import { userTableDefinitions } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' -import type { TableSchema } from '../types' +import type { TableSchema } from '@/lib/table/types' const logger = createLogger('TableWandEnricher') diff --git a/apps/sim/lib/table/query-builder/constants.ts b/apps/sim/lib/table/query-builder/constants.ts index d7e57c80b0a..ada8bc8c882 100644 --- a/apps/sim/lib/table/query-builder/constants.ts +++ b/apps/sim/lib/table/query-builder/constants.ts @@ -2,7 +2,7 @@ * Constants for table query builder UI (filtering and sorting). */ -export type { FilterRule, SortRule } from '../types' +export type { FilterRule, SortRule } from '@/lib/table/types' export const COMPARISON_OPERATORS = [ { value: 'eq', label: 'equals' }, diff --git a/apps/sim/lib/table/query-builder/converters.ts b/apps/sim/lib/table/query-builder/converters.ts index f0b96ce82b6..4dc29f4b0ae 100644 --- a/apps/sim/lib/table/query-builder/converters.ts +++ b/apps/sim/lib/table/query-builder/converters.ts @@ -4,7 +4,14 @@ import { generateShortId } from '@sim/utils/id' import { isRecordLike } from '@sim/utils/object' -import type { Filter, FilterRule, JsonValue, Sort, SortDirection, SortRule } from '../types' +import type { + Filter, + FilterRule, + JsonValue, + Sort, + SortDirection, + SortRule, +} from '@/lib/table/types' /** Converts UI filter rules to a Filter object for API queries. */ export function filterRulesToFilter(rules: FilterRule[]): Filter | null { diff --git a/apps/sim/lib/table/query-builder/index.ts b/apps/sim/lib/table/query-builder/index.ts index a6b9a46fec4..7ad04c58aa1 100644 --- a/apps/sim/lib/table/query-builder/index.ts +++ b/apps/sim/lib/table/query-builder/index.ts @@ -2,6 +2,6 @@ * Query builder UI utilities for filtering and sorting tables. */ -export * from './constants' -export * from './converters' -export * from './use-query-builder' +export * from '@/lib/table/query-builder/constants' +export * from '@/lib/table/query-builder/converters' +export * from '@/lib/table/query-builder/use-query-builder' diff --git a/apps/sim/lib/table/query-builder/use-query-builder.ts b/apps/sim/lib/table/query-builder/use-query-builder.ts index 79b2548086d..623ec4cd8fd 100644 --- a/apps/sim/lib/table/query-builder/use-query-builder.ts +++ b/apps/sim/lib/table/query-builder/use-query-builder.ts @@ -4,14 +4,14 @@ import { useCallback } from 'react' import { generateShortId } from '@sim/utils/id' -import type { ColumnOption } from '../types' import { COMPARISON_OPERATORS, type FilterRule, LOGICAL_OPERATORS, SORT_DIRECTIONS, type SortRule, -} from './constants' +} from '@/lib/table/query-builder/constants' +import type { ColumnOption } from '@/lib/table/types' const comparisonOptions: ColumnOption[] = COMPARISON_OPERATORS.map((op) => ({ value: op.value, diff --git a/apps/sim/lib/table/rows/executions.ts b/apps/sim/lib/table/rows/executions.ts new file mode 100644 index 00000000000..5555b856178 --- /dev/null +++ b/apps/sim/lib/table/rows/executions.ts @@ -0,0 +1,298 @@ +/** + * Row-executions (workflow-group results) internals for the table service layer. + * + * Internal module: not exposed via the `@/lib/table` barrel. Consumers import + * directly from `@/lib/table/rows/executions`. + */ + +import { tableRowExecutions } from '@sim/db/schema' +import { and, eq, inArray, type SQL, sql } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { getColumnId } from '@/lib/table/column-keys' +import { areGroupDepsSatisfied } from '@/lib/table/deps' +import type { + RowData, + RowExecutionMetadata, + RowExecutions, + TableRow, + TableSchema, +} from '@/lib/table/types' + +/** + * Loads `tableRowExecutions` rows for the given row ids and groups them into a + * `Map` suitable for plugging into `TableRow.executions`. + */ +export async function loadExecutionsByRow( + trx: DbOrTx, + rowIds: Iterable +): Promise> { + const ids = Array.from(new Set(rowIds)) + const result = new Map() + if (ids.length === 0) return result + const rows = await trx + .select() + .from(tableRowExecutions) + .where(inArray(tableRowExecutions.rowId, ids)) + for (const r of rows) { + const existing = result.get(r.rowId) ?? {} + const meta: RowExecutionMetadata = { + status: r.status as RowExecutionMetadata['status'], + executionId: r.executionId ?? null, + jobId: r.jobId ?? null, + workflowId: r.workflowId, + error: r.error ?? null, + ...(r.runningBlockIds && r.runningBlockIds.length > 0 + ? { runningBlockIds: r.runningBlockIds } + : {}), + ...(r.blockErrors && Object.keys(r.blockErrors as Record).length > 0 + ? { blockErrors: r.blockErrors as Record } + : {}), + ...(r.cancelledAt ? { cancelledAt: r.cancelledAt.toISOString() } : {}), + } + existing[r.groupId] = meta + result.set(r.rowId, existing) + } + return result +} + +/** Convenience: load executions for one row, returning `{}` when missing. */ +export async function loadExecutionsForRow(trx: DbOrTx, rowId: string): Promise { + const byRow = await loadExecutionsByRow(trx, [rowId]) + return byRow.get(rowId) ?? {} +} + +/** + * Derive automatic clears + cancellation candidates from a row's data patch. + * + * Walks `schema.workflowGroups` left-to-right with a propagating `dirtied` + * column set. For each group whose deps overlap the dirty set, decide to + * clear (terminal exec) or cancel+rerun (in-flight exec), then add the + * group's outputs to the dirty set so later groups in the chain see them + * as dirty too. This models transitive dep chains as a single forward pass — + * editing column A propagates through group 1 (deps on A) to group 2 (deps + * on group 1's output) without explicit DAG traversal. + * + * Returns: + * - `executionsPatch`: caller's patch + nulls for cleared groups (or + * undefined if nothing applied). + * - `inFlightDownstreamGroups`: groups whose dep was dirtied and that are + * currently in-flight. Cancel-and-restart is the caller's job. + * + * Assumption: `workflowGroups[]` is in topological order — a group's deps + * may only reference columns to its left (enforced by `workflow-sidebar`'s + * "Run after" picker + the reorder scrub via `stripGroupDeps`). Violating + * this would silently miss the propagation. + */ +export function deriveExecClearsForDataPatch( + dataPatch: RowData, + schema: TableSchema, + existingExecutions: RowExecutions, + callerPatch: Record | undefined, + mergedData: RowData +): { + executionsPatch: Record | undefined + inFlightDownstreamGroups: string[] +} { + const dirtied = new Set(Object.keys(dataPatch)) + const groupsToClear = new Set() + const inFlightDownstreamGroups: string[] = [] + + // Own-output clears: when the user wipes a workflow output column, drop + // that group's exec entry so the auto-fire reactor re-arms the cell. + // Also flags the cleared output column as dirty so transitive downstream + // groups see it. + for (const [columnId, value] of Object.entries(dataPatch)) { + const cleared = value === null || value === undefined || value === '' + if (!cleared) continue + const col = schema.columns.find((c) => getColumnId(c) === columnId) + if (col?.workflowGroupId) groupsToClear.add(col.workflowGroupId) + } + + // Left-to-right walk, propagating dirty columns forward. + const groups = schema.workflowGroups ?? [] + const afterRow = { data: mergedData } as TableRow + for (const group of groups) { + const deps = group.dependencies?.columns ?? [] + const depMatched = deps.some((d) => dirtied.has(d)) + if (!depMatched) continue + + // A dep column changed, but if the group's deps are no longer satisfied + // after the patch — a checkbox was unchecked or a text dep cleared — there's + // nothing to recompute. Leave the prior result alone instead of re-arming or + // cancelling it; only checking a box / filling a dep drives downstream work. + if (!areGroupDepsSatisfied(group, afterRow)) continue + + const exec = existingExecutions[group.id] + if (exec) { + const status = exec.status + if (status === 'completed' || status === 'error' || status === 'cancelled') { + groupsToClear.add(group.id) + } else if (status === 'queued' || status === 'running' || status === 'pending') { + inFlightDownstreamGroups.push(group.id) + } + } else { + // No exec entry yet — `mode: 'new'` already covers this group. We + // still propagate the dirty signal forward so later groups in the + // chain see this group's outputs as dirty too. + groupsToClear.add(group.id) + } + + // Propagate: this group is about to be re-computed, so groups whose + // deps reference its output columns are also dirty. + for (const out of group.outputs) dirtied.add(out.columnName) + } + + if (groupsToClear.size === 0) { + return { executionsPatch: callerPatch, inFlightDownstreamGroups } + } + const merged: Record = { ...(callerPatch ?? {}) } + for (const gid of groupsToClear) { + if (!(gid in merged)) merged[gid] = null + } + return { executionsPatch: merged, inFlightDownstreamGroups } +} + +/** Merges an `executionsPatch` into the row's existing executions blob. */ +export function applyExecutionsPatch( + existing: RowExecutions, + patch: Record | undefined +): RowExecutions { + if (!patch) return existing + const next: RowExecutions = { ...existing } + for (const [gid, value] of Object.entries(patch)) { + if (value === null) { + delete next[gid] + } else { + next[gid] = value + } + } + return next +} + +/** + * Writes a per-group execution patch for one row against the `tableRowExecutions` + * sidecar. Non-null values upsert into the table; nulls delete the entry. When + * `guard` is set, the upsert is gated to: + * - reject if a `cancelled` row for the same execution already exists, and + * - reject if the row exists but is owned by a different executionId + * (with carve-outs for missing rows and null executionIds — the dispatcher's + * pre-batch `pending` stamp leaves executionId unset so the first cell-task + * can claim). + * + * Returns `'guard-rejected'` when the guarded group's upsert affected 0 rows + * (callers signal failure to the cell-task path). Returns `'wrote'` otherwise. + */ +export async function writeExecutionsPatch( + trx: DbOrTx, + tableId: string, + rowId: string, + patch: Record | undefined, + guard?: { groupId: string; executionId: string } +): Promise<'wrote' | 'guard-rejected'> { + if (!patch) return 'wrote' + const entries = Object.entries(patch) + if (entries.length === 0) return 'wrote' + + for (const [gid, value] of entries) { + if (value === null) { + await trx + .delete(tableRowExecutions) + .where(and(eq(tableRowExecutions.rowId, rowId), eq(tableRowExecutions.groupId, gid)) as SQL) + continue + } + const insertValues = { + tableId, + rowId, + groupId: gid, + status: value.status, + executionId: value.executionId, + jobId: value.jobId, + workflowId: value.workflowId, + error: value.error, + runningBlockIds: value.runningBlockIds ?? [], + blockErrors: value.blockErrors ?? {}, + cancelledAt: value.cancelledAt ? new Date(value.cancelledAt) : null, + updatedAt: new Date(), + } as const + + const isGuarded = guard && guard.groupId === gid + if (isGuarded) { + // Gate by guard semantics. The original JSONB guard had two AND'd + // clauses; we collapse them onto the upsert's WHERE so a non-matching + // existing row leaves the table untouched and we observe 0 affected. + const guardExecutionId = guard.executionId + const updated = await trx + .insert(tableRowExecutions) + .values(insertValues) + .onConflictDoUpdate({ + target: [tableRowExecutions.rowId, tableRowExecutions.groupId], + set: { + status: insertValues.status, + executionId: insertValues.executionId, + jobId: insertValues.jobId, + workflowId: insertValues.workflowId, + error: insertValues.error, + runningBlockIds: insertValues.runningBlockIds, + blockErrors: insertValues.blockErrors, + cancelledAt: insertValues.cancelledAt, + updatedAt: insertValues.updatedAt, + }, + where: and( + // Reject any guarded worker write when the cell is `cancelled` — a + // stop click wrote it authoritatively. SQL mirror of `isExecCancelled` + // (deps.ts). Status-only (not executionId-scoped): the cancel can + // only carry the pre-stamp's executionId (often null), so matching on + // id would let the worker's real-id claim resurrect a killed cell. + sql`${tableRowExecutions.status} <> 'cancelled'`, + // Stale-worker: the cell's active run has moved on. Carve-outs + // permit a fresh worker to take over when the row's executionId + // is unset (dispatcher's pre-batch `pending` stamp). + sql`(${tableRowExecutions.executionId} IS NULL OR ${tableRowExecutions.executionId} = ${guardExecutionId})` + ) as SQL, + }) + .returning({ rowId: tableRowExecutions.rowId }) + if (updated.length === 0) return 'guard-rejected' + continue + } + + await trx + .insert(tableRowExecutions) + .values(insertValues) + .onConflictDoUpdate({ + target: [tableRowExecutions.rowId, tableRowExecutions.groupId], + set: { + status: insertValues.status, + executionId: insertValues.executionId, + jobId: insertValues.jobId, + workflowId: insertValues.workflowId, + error: insertValues.error, + runningBlockIds: insertValues.runningBlockIds, + blockErrors: insertValues.blockErrors, + cancelledAt: insertValues.cancelledAt, + updatedAt: insertValues.updatedAt, + }, + }) + } + + return 'wrote' +} + +/** + * Strips the given workflow group ids from every row's executions on a table — + * used by the column / group delete paths so stale running/queued exec records + * don't linger and inflate counters after the group is gone. The caller wraps + * in their own transaction. + */ +export async function stripGroupExecutions( + trx: DbOrTx, + tableId: string, + groupIds: Iterable +): Promise { + const ids = Array.from(new Set(groupIds)) + if (ids.length === 0) return + await trx + .delete(tableRowExecutions) + .where( + and(eq(tableRowExecutions.tableId, tableId), inArray(tableRowExecutions.groupId, ids)) as SQL + ) +} diff --git a/apps/sim/lib/table/rows/ordering.ts b/apps/sim/lib/table/rows/ordering.ts new file mode 100644 index 00000000000..c1408744329 --- /dev/null +++ b/apps/sim/lib/table/rows/ordering.ts @@ -0,0 +1,557 @@ +/** + * Row position / fractional-ordering internals for the table service layer. + * + * Internal module: only the import/delete-runner entry points are exposed via + * the `@/lib/table/rows/ordering` path. Not re-exported through the + * `@/lib/table` barrel. + */ + +import { db } from '@sim/db' +import { userTableRows } from '@sim/db/schema' +import { and, asc, desc, eq, gt, gte, inArray, lt, lte, type SQL, sql } from 'drizzle-orm' +import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/feature-flags' +import type { DbOrTx } from '@/lib/db/types' +import { TABLE_LIMITS } from '@/lib/table/constants' +import { keyBetween, nKeysBetween } from '@/lib/table/order-key' +import { type DbExecutor, type DbTransaction, withSeqscanOff } from '@/lib/table/planner' +import { setTableTxTimeouts } from '@/lib/table/tx' +import type { RowData } from '@/lib/table/types' + +/** + * Starting `position` for an append import — `max(position) + 1`, or 0 when empty. Read once, + * unlocked, before streaming: the import worker is the table's sole writer, so it can assign + * contiguous positions from this offset without per-batch position scans. + */ +export async function nextImportStartPosition(tableId: string): Promise { + const [{ maxPos }] = await db + .select({ + maxPos: sql`coalesce(max(${userTableRows.position}), -1)`.mapWith(Number), + }) + .from(userTableRows) + .where(eq(userTableRows.tableId, tableId)) + return maxPos + 1 +} + +/** + * Append anchor `order_key` for an import — `max(order_key)`, or null when empty. Read once, + * unlocked, before streaming (the import worker is the table's sole writer); each batch threads + * the previous batch's last key forward so no per-batch max scan is needed. + */ +export async function nextImportStartOrderKey(tableId: string): Promise { + return maxOrderKey(db, tableId) +} + +/** + * Serializes writers that assign `position` for the same table. The row-count + * trigger (migration 0198) serializes capacity via a row lock on + * `user_table_definitions`, but it fires AFTER INSERT, so two concurrent + * auto-positioned inserts could read the same snapshot and assign the same + * position (the `(table_id, position)` index is non-unique). This advisory lock + * restores per-table serialization. Released at COMMIT/ROLLBACK. + */ +export async function acquireRowOrderLock(trx: DbTransaction, tableId: string) { + await trx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_rows_pos:${tableId}`}, 0))` + ) +} + +/** Next append position for a table (max(position) + 1, or 0 if empty). */ +export async function nextRowPosition(trx: DbTransaction, tableId: string): Promise { + const [{ maxPos }] = await trx + .select({ + maxPos: sql`coalesce(max(${userTableRows.position}), -1)`.mapWith(Number), + }) + .from(userTableRows) + .where(eq(userTableRows.tableId, tableId)) + return maxPos + 1 +} + +/** Largest `order_key` for a table, or `null` when empty — the append anchor for new keys. */ +export async function maxOrderKey(executor: DbOrTx, tableId: string): Promise { + const [{ maxKey }] = await executor + .select({ maxKey: sql`max(${userTableRows.orderKey})` }) + .from(userTableRows) + .where(eq(userTableRows.tableId, tableId)) + return maxKey ?? null +} + +/** Shifts every row at or after `position` up by one (`position + 1`). */ +export async function shiftRowsUpFrom(trx: DbTransaction, tableId: string, position: number) { + await trx + .update(userTableRows) + .set({ position: sql`position + 1` }) + .where(and(eq(userTableRows.tableId, tableId), gte(userTableRows.position, position))) +} + +/** Shifts every row after `position` down by one (`position - 1`). */ +export async function shiftRowsDownAfter(trx: DbTransaction, tableId: string, position: number) { + await trx + .update(userTableRows) + .set({ position: sql`position - 1` }) + .where(and(eq(userTableRows.tableId, tableId), gt(userTableRows.position, position))) +} + +/** + * Reserves the `position` for a single inserted row and returns where to INSERT. + * Acquires the row-order lock, then opens a slot at `requestedPosition` (shifting + * the occupant + tail up) or computes the append position. Caller runs inside a + * transaction. + */ +export async function reserveInsertPosition( + trx: DbTransaction, + tableId: string, + requestedPosition?: number +): Promise { + await acquireRowOrderLock(trx, tableId) + if (requestedPosition === undefined) { + return nextRowPosition(trx, tableId) + } + const [existing] = await trx + .select({ id: userTableRows.id }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, requestedPosition))) + .limit(1) + if (existing) { + await shiftRowsUpFrom(trx, tableId, requestedPosition) + } + return requestedPosition +} + +/** + * Reserves positions for a batch of `count` rows. Opens each requested slot + * (ascending, preserving prior gaps) and returns the requested positions in + * original order; otherwise returns a contiguous append range. + */ +export async function reserveBatchPositions( + trx: DbTransaction, + tableId: string, + count: number, + requestedPositions?: number[] +): Promise { + await acquireRowOrderLock(trx, tableId) + if (requestedPositions && requestedPositions.length > 0) { + for (const pos of [...requestedPositions].sort((a, b) => a - b)) { + await shiftRowsUpFrom(trx, tableId, pos) + } + return requestedPositions + } + const start = await nextRowPosition(trx, tableId) + return Array.from({ length: count }, (_, i) => start + i) +} + +/** + * Recompacts row positions to be contiguous after a bulk delete. With + * `minDeletedPos`, only rows at/after it are re-numbered; single-row deletes use + * the cheaper {@link shiftRowsDownAfter}. + */ +export async function compactPositions( + trx: DbTransaction, + tableId: string, + minDeletedPos?: number +) { + if (minDeletedPos === undefined) { + await trx.execute(sql` + UPDATE user_table_rows t + SET position = r.new_pos + FROM ( + SELECT id, ROW_NUMBER() OVER (ORDER BY position) - 1 AS new_pos + FROM user_table_rows + WHERE table_id = ${tableId} + ) r + WHERE t.id = r.id AND t.table_id = ${tableId} AND t.position != r.new_pos + `) + return + } + await trx.execute(sql` + UPDATE user_table_rows t + SET position = r.new_pos + FROM ( + SELECT id, ${minDeletedPos}::int + ROW_NUMBER() OVER (ORDER BY position) - 1 AS new_pos + FROM user_table_rows + WHERE table_id = ${tableId} AND position >= ${minDeletedPos} + ) r + WHERE t.id = r.id AND t.table_id = ${tableId} AND t.position != r.new_pos + `) +} + +/** + * Computes the fractional `order_key` for a row inserted at the integer + * `requestedPosition` (or appended when omitted). Used by position-based callers + * (mothership tool, v1 API, undo position-fallback, transient old clients). + * + * The neighbor at slot `s` is resolved differently per flag state: + * - **off**: `WHERE position = s` (positions are contiguous, so the row at + * position `s` is the `s`-th row — an indexed O(1) lookup). + * - **on**: the `s`-th row in `order_key, id` order (`OFFSET s`) — positions are + * gappy and non-authoritative, so `position = s` would miss; the visual + * ordinal is the key's ordinal. O(s), acceptable for these low-volume callers. + * + * Caller holds the row-order lock. + */ +export async function resolveInsertOrderKey( + trx: DbTransaction, + tableId: string, + requestedPosition?: number +): Promise { + const orderKeyAtSlot = async (slot: number): Promise => { + if (slot < 0) return null + if (isTablesFractionalOrderingEnabled) { + const [r] = await trx + .select({ orderKey: userTableRows.orderKey }) + .from(userTableRows) + .where(eq(userTableRows.tableId, tableId)) + .orderBy(asc(userTableRows.orderKey), asc(userTableRows.id)) + .limit(1) + .offset(slot) + return r?.orderKey ?? null + } + const [r] = await trx + .select({ orderKey: userTableRows.orderKey }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, slot))) + .limit(1) + return r?.orderKey ?? null + } + if (requestedPosition === undefined) { + return keyBetween(await maxOrderKey(trx, tableId), null) + } + const lo = await orderKeyAtSlot(requestedPosition - 1) + const hi = await orderKeyAtSlot(requestedPosition) + return keyBetween(lo, hi) +} + +/** + * Resolves the `order_key` for an insert expressed by an anchor row id — + * `afterRowId` (place directly after) or `beforeRowId` (directly before). Finds + * the anchor and its adjacent key via the `(table_id, order_key, id)` index + * (O(1)) and mints a key between them. Also returns a legacy integer `position` + * (anchor's position ±) so the flag-off shift path still works. Caller holds the + * row-order lock. + */ +export async function resolveInsertByNeighbor( + trx: DbTransaction, + tableId: string, + afterRowId?: string, + beforeRowId?: string +): Promise<{ orderKey: string; position: number }> { + const anchorId = afterRowId ?? beforeRowId! + const [anchor] = await trx + .select({ orderKey: userTableRows.orderKey, position: userTableRows.position }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.id, anchorId))) + .limit(1) + // The client targets a specific neighbor; a missing one (concurrent delete / + // stale view) is an error, not a silent insert at the front. + if (!anchor) throw new Error(`Row not found: ${anchorId}`) + const anchorKey = anchor.orderKey ?? null + // A null key on the anchor means the table isn't backfilled. With the flag on + // (key is authoritative) the adjacent-key lookup below can't work — fail + // loudly rather than mint a wrong key. Flag off keeps `position` authoritative, + // so a best-effort key here is fine (the backfill re-keys before the flip). + if (anchorKey === null && isTablesFractionalOrderingEnabled) { + throw new Error(`Row ${anchorId} has no order_key yet (table not backfilled)`) + } + + if (afterRowId) { + // hi = the smallest key strictly GREATER than the anchor key. Comparing keys + // (not the `(order_key, id)` row tuple) skips past any sibling that shares the + // anchor's key, so `keyBetween` always gets strictly-ordered bounds and can't + // throw on a stray duplicate. Identical to the row tuple when keys are distinct. + // A null anchorKey (flag off, un-backfilled) has no key to compare — leave the + // upper bound open, matching the prior best-effort behavior. + let nextKey: string | null = null + if (anchorKey !== null) { + const [next] = await trx + .select({ orderKey: userTableRows.orderKey }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), gt(userTableRows.orderKey, anchorKey))) + .orderBy(asc(userTableRows.orderKey)) + .limit(1) + nextKey = next?.orderKey ?? null + } + return { + orderKey: keyBetween(anchorKey, nextKey), + position: anchor.position + 1, + } + } + + // beforeRowId: lo = the largest key strictly LESS than the anchor key (distinct, + // same rationale as the afterRowId branch above). + let prevKey: string | null = null + if (anchorKey !== null) { + const [prev] = await trx + .select({ orderKey: userTableRows.orderKey }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), lt(userTableRows.orderKey, anchorKey))) + .orderBy(desc(userTableRows.orderKey)) + .limit(1) + prevKey = prev?.orderKey ?? null + } + return { + orderKey: keyBetween(prevKey, anchorKey), + position: anchor.position, + } +} + +/** + * Computes fractional `order_key`s for a batch insert. With no `positions`, + * appends a contiguous run after the current max key. With explicit `positions` + * (undo restore), keys each row between its pre-shift position neighbors — + * correct because requested positions are distinct. Caller holds the lock. + * + * The explicit-`positions` path is meaningful only when `position` is + * authoritative (flag off): with the flag on, a saved `position` is a gappy + * column value, not a visual rank, so feeding it to {@link resolveInsertOrderKey} + * (which reads `position` as an `OFFSET` rank under the flag) would mint keys at + * the wrong ranks. Callers needing exact placement under the flag pass + * `orderKeys` (handled before this function); here we just append a run. + */ +export async function resolveBatchInsertOrderKeys( + trx: DbTransaction, + tableId: string, + count: number, + positions?: number[] +): Promise { + if (!positions || positions.length === 0 || isTablesFractionalOrderingEnabled) { + return nKeysBetween(await maxOrderKey(trx, tableId), null, count) + } + const keys: string[] = [] + for (const pos of positions) { + keys.push(await resolveInsertOrderKey(trx, tableId, pos)) + } + return keys +} + +/** + * Inserts a single row in its own transaction. Always assigns a fractional + * `order_key`. When the fractional-ordering flag is on, `order_key` is + * authoritative and `position` is a best-effort append (no O(N) shift); when + * off, `position` is reserved as before (shifting to open the slot). Validation + * and side-effect dispatch stay with the caller; capacity is enforced by the + * `increment_user_table_row_count` trigger. + */ +export async function insertOrderedRow(params: { + tableId: string + workspaceId: string + data: RowData + rowId: string + position?: number + afterRowId?: string + beforeRowId?: string + createdBy?: string + now: Date +}): Promise<{ + id: string + data: RowData + position: number + orderKey: string | null + createdAt: Date + updatedAt: Date +}> { + const { tableId, workspaceId, data, rowId, position, afterRowId, beforeRowId, createdBy, now } = + params + const [row] = await db.transaction(async (trx) => { + await setTableTxTimeouts(trx) + await acquireRowOrderLock(trx, tableId) + + // Resolve the order key (and a legacy slot position for the flag-off shift + // path) from neighbor ids when given, else from the requested position. + let orderKey: string + let slotPosition = position + if (afterRowId || beforeRowId) { + const resolved = await resolveInsertByNeighbor(trx, tableId, afterRowId, beforeRowId) + orderKey = resolved.orderKey + slotPosition = resolved.position + } else { + orderKey = await resolveInsertOrderKey(trx, tableId, position) + } + + let targetPosition: number + if (isTablesFractionalOrderingEnabled) { + // order_key is authoritative — keep a best-effort, no-shift position. + targetPosition = await nextRowPosition(trx, tableId) + } else if (slotPosition !== undefined) { + const [existing] = await trx + .select({ id: userTableRows.id }) + .from(userTableRows) + .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, slotPosition))) + .limit(1) + if (existing) await shiftRowsUpFrom(trx, tableId, slotPosition) + targetPosition = slotPosition + } else { + targetPosition = await nextRowPosition(trx, tableId) + } + + return trx + .insert(userTableRows) + .values({ + id: rowId, + tableId, + workspaceId, + data, + position: targetPosition, + orderKey, + createdAt: now, + updatedAt: now, + ...(createdBy ? { createdBy } : {}), + }) + .returning() + }) + return { + id: row.id, + data: row.data as RowData, + position: row.position, + orderKey: row.orderKey, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } +} + +/** + * Deletes a single row by id in its own transaction, then closes the positional + * gap. Returns `false` when no row matched. + */ +export async function deleteOrderedRow(params: { + tableId: string + rowId: string + workspaceId: string +}): Promise { + const { tableId, rowId, workspaceId } = params + return db.transaction(async (trx) => { + await setTableTxTimeouts(trx) + const [deleted] = await trx + .delete(userTableRows) + .where( + and( + eq(userTableRows.id, rowId), + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId) + ) + ) + .returning({ position: userTableRows.position }) + if (!deleted) return false + // Fractional ordering: deleting a row never changes another row's order_key, + // so the O(N) position reshift is skipped entirely. + if (!isTablesFractionalOrderingEnabled) { + await shiftRowsDownAfter(trx, tableId, deleted.position) + } + return true + }) +} + +/** + * Deletes the given row ids in batches within one transaction, then recompacts + * positions from the earliest deleted slot. Returns the deleted rows (id + prior + * position). The caller resolves which ids to delete (used by both delete-by-ids + * and delete-by-filter). + */ +export async function deleteOrderedRowsByIds(params: { + tableId: string + workspaceId: string + rowIds: string[] +}): Promise<{ id: string; position: number }[]> { + const { tableId, workspaceId, rowIds } = params + if (rowIds.length === 0) return [] + return db.transaction(async (trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + const deleted: { id: string; position: number }[] = [] + for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) { + const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE) + const rows = await trx + .delete(userTableRows) + .where( + and( + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId), + inArray(userTableRows.id, batch) + ) + ) + .returning({ id: userTableRows.id, position: userTableRows.position }) + deleted.push(...rows) + } + // Fractional ordering: deletes leave order_key untouched, so no recompaction. + if (!isTablesFractionalOrderingEnabled && deleted.length > 0) { + const minDeletedPos = deleted.reduce( + (min, r) => (r.position < min ? r.position : min), + deleted[0].position + ) + await compactPositions(trx, tableId, minDeletedPos) + } + return deleted + }) +} + +/** + * Selects one page of row ids to delete for the async delete-job worker: base scope plus a + * `created_at <= cutoff` floor (so rows inserted after the job started are never selected) and + * the caller's optional filter clause. Keyset paginated on `id` via `afterId` so excluded rows + * (which are skipped, not deleted) still advance the cursor — no OFFSET, no risk of looping on a + * fully-excluded page. + */ +export async function selectRowIdPage(params: { + tableId: string + workspaceId: string + cutoff: Date + filterClause?: SQL + afterId?: string + limit: number +}): Promise { + const { tableId, workspaceId, cutoff, filterClause, afterId, limit } = params + const selectPage = (executor: DbExecutor) => + executor + .select({ id: userTableRows.id }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId), + lte(userTableRows.createdAt, cutoff), + afterId ? gt(userTableRows.id, afterId) : undefined, + filterClause + ) + ) + .orderBy(asc(userTableRows.id)) + .limit(limit) + // A jsonb filter is unestimatable, so the planner would seq-scan the whole shared relation + // per page (12.6s measured) — keep it on the tenant's (table_id, id) index. + const rows = filterClause + ? await withSeqscanOff(async (trx) => selectPage(trx)) + : await selectPage(db) + return rows.map((r) => r.id) +} + +/** + * Deletes one page of rows for the async delete-job worker, committing each `DELETE_BATCH_SIZE` + * chunk in its own short transaction. One statement per transaction bounds how long the + * statement-level row_count trigger's lock on the definition row is held (a page-wide transaction + * held it for the entire page, starving concurrent inserts and overrunning `statement_timeout`), + * and a mid-page failure loses at most one uncommitted batch — the keyset walker (or a task + * retry) re-walks whatever remains. Skips legacy position compaction: under fractional ordering + * it's unnecessary, and in the legacy path `position` gaps are harmless — rows still order by + * position. Returns the count deleted. + */ +export async function deletePageByIds( + tableId: string, + workspaceId: string, + rowIds: string[] +): Promise { + let deleted = 0 + for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) { + const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE) + const rows = await db.transaction(async (trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + return trx + .delete(userTableRows) + .where( + and( + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId), + inArray(userTableRows.id, batch) + ) + ) + .returning({ id: userTableRows.id }) + }) + deleted += rows.length + } + return deleted +} diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts new file mode 100644 index 00000000000..5f08305ade6 --- /dev/null +++ b/apps/sim/lib/table/rows/service.ts @@ -0,0 +1,1676 @@ +/** + * Row CRUD + query operations for the table service layer. + * + * Holds the row-write group (`insertRow`, `batchInsertRows`, `upsertRow`, + * `updateRow`, `deleteRow`, the bulk/filter variants, `replaceTableRows`) and the + * row-read group (`queryRows`, `getRowById`, `findRowMatches`). Mirrors the + * `@/lib/table` service conventions: plain exported async functions, drizzle + * inline, no repository pattern. + * + * Re-exported through the `@/lib/table` barrel. + */ + +import { db } from '@sim/db' +import { tableJobs, userTableRows } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, count, eq, inArray, lte, notInArray, type SQL, sql } from 'drizzle-orm' +import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/feature-flags' +import { getColumnId } from '@/lib/table/column-keys' +import { TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' +import { nKeysBetween } from '@/lib/table/order-key' +import { type DbExecutor, type DbTransaction, withSeqscanOff } from '@/lib/table/planner' +import { + applyExecutionsPatch, + deriveExecClearsForDataPatch, + loadExecutionsByRow, + loadExecutionsForRow, + writeExecutionsPatch, +} from '@/lib/table/rows/executions' +import { + acquireRowOrderLock, + deleteOrderedRow, + deleteOrderedRowsByIds, + insertOrderedRow, + nextRowPosition, + reserveBatchPositions, + reserveInsertPosition, + resolveBatchInsertOrderKeys, + resolveInsertOrderKey, +} from '@/lib/table/rows/ordering' +import { buildFilterClause, buildSortClause, escapeLikePattern } from '@/lib/table/sql' +import { fireTableTrigger } from '@/lib/table/trigger' +import { scaledStatementTimeoutMs, setTableTxTimeouts } from '@/lib/table/tx' +import type { + BatchInsertData, + BatchUpdateByIdData, + BulkDeleteByIdsData, + BulkDeleteByIdsResult, + BulkDeleteData, + BulkOperationResult, + BulkUpdateData, + ColumnDefinition, + Filter, + InsertRowData, + QueryOptions, + QueryResult, + ReplaceRowsData, + ReplaceRowsResult, + RowData, + RowExecutionMetadata, + RowExecutions, + Sort, + TableDefinition, + TableDeleteJobPayload, + TableRow, + UpdateRowData, + UpsertResult, + UpsertRowData, +} from '@/lib/table/types' +import { + checkBatchUniqueConstraintsDb, + checkUniqueConstraintsDb, + coerceRowToSchema, + coerceRowValues, + getUniqueColumns, + validateRowSize, +} from '@/lib/table/validation' +import { cancelWorkflowGroupRuns, runWorkflowColumn } from '@/lib/table/workflow-columns' + +const logger = createLogger('TableRowsService') + +/** + * Inserts a single row into a table. + * + * @param data - Row insertion data + * @param table - Table definition (to avoid re-fetching) + * @param requestId - Request ID for logging + * @returns Inserted row + * @throws Error if validation fails or capacity exceeded + */ +export async function insertRow( + data: InsertRowData, + table: TableDefinition, + requestId: string +): Promise { + // Validate row size + const sizeValidation = validateRowSize(data.data) + if (!sizeValidation.valid) { + throw new Error(sizeValidation.errors.join(', ')) + } + + // Validate against schema + const schemaValidation = coerceRowToSchema(data.data, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) + } + + // Check unique constraints using optimized database query + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0) { + const uniqueValidation = await checkUniqueConstraintsDb(data.tableId, data.data, table.schema) + if (!uniqueValidation.valid) { + throw new Error(uniqueValidation.errors.join(', ')) + } + } + + const rowId = `row_${generateId().replace(/-/g, '')}` + const now = new Date() + + // Capacity enforcement lives in the `increment_user_table_row_count` trigger + // (migration 0198): a single conditional UPDATE on user_table_definitions + // increments row_count iff row_count < max_rows, taking the row lock + // atomically. No app-level FOR UPDATE / COUNT needed. + const row = await insertOrderedRow({ + tableId: data.tableId, + workspaceId: data.workspaceId, + data: data.data, + rowId, + position: data.position, + afterRowId: data.afterRowId, + beforeRowId: data.beforeRowId, + createdBy: data.userId, + now, + }) + + logger.info(`[${requestId}] Inserted row ${rowId} into table ${data.tableId}`) + + const insertedRow: TableRow = { + id: row.id, + data: row.data as RowData, + executions: {}, + position: row.position, + orderKey: row.orderKey ?? undefined, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } + + void fireTableTrigger( + data.tableId, + table.name, + 'insert', + [insertedRow], + null, + table.schema, + requestId + ) + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: [insertedRow.id], + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: data.userId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (insertRow) failed:`, err)) + + return insertedRow +} + +/** + * Inserts multiple rows into a table. + * + * @param data - Batch insertion data + * @param table - Table definition + * @param requestId - Request ID for logging + * @returns Array of inserted rows + * @throws Error if validation fails or capacity exceeded + */ +export async function batchInsertRows( + data: BatchInsertData, + table: TableDefinition, + requestId: string +): Promise { + const result = await db.transaction((trx) => batchInsertRowsWithTx(trx, data, table, requestId)) + dispatchAfterBatchInsert(table, result, requestId, data.userId) + return result +} + +/** + * Transaction-bound variant of `batchInsertRows`. Validates rows and unique + * constraints, then performs INSERTs inside the provided transaction. Caller + * is responsible for opening the transaction. Use when row inserts must be + * atomic with other writes (e.g., schema mutations) on the same tx. + * + * Capacity enforcement lives in the `increment_user_table_row_count` trigger + * (migration 0198) — fires per row and raises `Maximum row limit (%) reached ...` + * if the cap is hit mid-batch. + */ +export async function batchInsertRowsWithTx( + trx: DbTransaction, + data: BatchInsertData, + table: TableDefinition, + requestId: string +): Promise { + for (let i = 0; i < data.rows.length; i++) { + const row = data.rows[i] + + const sizeValidation = validateRowSize(row) + if (!sizeValidation.valid) { + throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) + } + + const schemaValidation = coerceRowToSchema(row, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) + } + } + + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0) { + const uniqueResult = await checkBatchUniqueConstraintsDb( + data.tableId, + data.rows, + table.schema, + trx + ) + if (!uniqueResult.valid) { + const errorMessages = uniqueResult.errors + .map((e) => `Row ${e.row + 1}: ${e.errors.join(', ')}`) + .join('; ') + throw new Error(errorMessages) + } + } + + const now = new Date() + + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + + const buildRow = (rowData: RowData, position: number, orderKey: string) => ({ + id: `row_${generateId().replace(/-/g, '')}`, + tableId: data.tableId, + workspaceId: data.workspaceId, + data: rowData, + position, + orderKey, + createdAt: now, + updatedAt: now, + ...(data.userId ? { createdBy: data.userId } : {}), + }) + + await acquireRowOrderLock(trx, data.tableId) + // Undo restore passes exact saved keys; otherwise derive from positions/append. + const orderKeys = + data.orderKeys && data.orderKeys.length > 0 + ? data.orderKeys + : await resolveBatchInsertOrderKeys(trx, data.tableId, data.rows.length, data.positions) + let positions: number[] + if (isTablesFractionalOrderingEnabled) { + // order_key authoritative — best-effort append positions, no shift. + const start = await nextRowPosition(trx, data.tableId) + positions = Array.from({ length: data.rows.length }, (_, i) => start + i) + } else { + positions = await reserveBatchPositions(trx, data.tableId, data.rows.length, data.positions) + } + const rowsToInsert = data.rows.map((rowData, i) => buildRow(rowData, positions[i], orderKeys[i])) + const insertedRows = await trx.insert(userTableRows).values(rowsToInsert).returning() + + logger.info(`[${requestId}] Batch inserted ${data.rows.length} rows into table ${data.tableId}`) + + const result: TableRow[] = insertedRows.map((r) => ({ + id: r.id, + data: r.data as RowData, + executions: {}, + position: r.position, + orderKey: r.orderKey ?? undefined, + createdAt: r.createdAt, + updatedAt: r.updatedAt, + })) + + return result +} + +/** + * Side-effect dispatch for an insert batch. Caller fires this AFTER the + * surrounding transaction commits — `fireTableTrigger` and `runWorkflowColumn` + * both read through the global db connection, so firing inside the tx can see + * no rows and no-op. + */ +export function dispatchAfterBatchInsert( + table: TableDefinition, + result: TableRow[], + requestId: string, + actorUserId?: string | null +): void { + void fireTableTrigger(table.id, table.name, 'insert', result, null, table.schema, requestId) + // Scope to the newly-inserted row ids so the dispatcher doesn't walk every + // row in the table. After the sidecar migration, all existing rows have + // zero entries → `mode:'new'`'s `NOT EXISTS` filter would otherwise include + // them, dispatching workflows on every row in a populated table. + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: result.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: actorUserId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchInsertRows) failed:`, err)) +} + +/** + * Replaces all rows in a table with a new set of rows. Deletes existing rows + * and inserts the provided rows inside a single transaction so the table is + * never observed in an empty intermediate state by other readers. + * + * Validates each row against the schema, enforces unique constraints within the + * new rows (existing rows are deleted, so DB-side checks are unnecessary), and + * enforces `maxRows` before the replace executes. + * + * @param data - Replace data (rows to install) + * @param table - Table definition + * @param requestId - Request ID for logging + * @returns Count of rows deleted and inserted + * @throws Error if validation fails or capacity exceeded + */ +export async function replaceTableRows( + data: ReplaceRowsData, + table: TableDefinition, + requestId: string +): Promise { + return db.transaction((trx) => replaceTableRowsWithTx(trx, data, table, requestId)) +} + +/** + * Transaction-bound variant of `replaceTableRows`. Caller opens the transaction. + * Use when the replace must be atomic with other writes (e.g., schema mutations). + */ +export async function replaceTableRowsWithTx( + trx: DbTransaction, + data: ReplaceRowsData, + table: TableDefinition, + requestId: string +): Promise { + if (data.tableId !== table.id) { + throw new Error(`Table ID mismatch: ${data.tableId} vs ${table.id}`) + } + if (data.workspaceId !== table.workspaceId) { + throw new Error(`Workspace ID mismatch: ${data.workspaceId} does not own table ${data.tableId}`) + } + if (data.rows.length > table.maxRows) { + throw new Error( + `Cannot replace: ${data.rows.length} rows exceeds table row limit (${table.maxRows})` + ) + } + + for (let i = 0; i < data.rows.length; i++) { + const row = data.rows[i] + + const sizeValidation = validateRowSize(row) + if (!sizeValidation.valid) { + throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) + } + + const schemaValidation = coerceRowToSchema(row, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) + } + } + + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0 && data.rows.length > 0) { + const seen = new Map>() + for (const col of uniqueColumns) { + seen.set(col.name, new Map()) + } + for (let i = 0; i < data.rows.length; i++) { + const row = data.rows[i] + for (const col of uniqueColumns) { + const value = row[col.name] + if (value === null || value === undefined) continue + const normalized = typeof value === 'string' ? value.toLowerCase() : JSON.stringify(value) + const map = seen.get(col.name)! + if (map.has(normalized)) { + throw new Error( + `Row ${i + 1}: Column "${col.name}" must be unique. Value "${String(value)}" duplicates row ${map.get(normalized)! + 1} in batch` + ) + } + map.set(normalized, i) + } + } + } + + const now = new Date() + + const totalRowWork = Math.max(0, table.rowCount ?? 0) + data.rows.length + const statementMs = scaledStatementTimeoutMs(totalRowWork, { + baseMs: 120_000, + perRowMs: 3, + }) + + await setTableTxTimeouts(trx, { statementMs }) + + // Serialize concurrent replaces (and concurrent auto-position inserts) on the + // same table. Without this, two concurrent replaces each see their own MVCC + // snapshot for the DELETE; the second's DELETE would not observe rows the + // first inserted, so both transactions commit and the table ends up with + // the union of both row sets instead of only the last caller's rows. + await acquireRowOrderLock(trx, data.tableId) + + const deletedRows = await trx + .delete(userTableRows) + .where(eq(userTableRows.tableId, data.tableId)) + .returning({ id: userTableRows.id }) + + let insertedCount = 0 + if (data.rows.length > 0) { + // All prior rows were just deleted — assign a fresh contiguous key run. + const orderKeys = nKeysBetween(null, null, data.rows.length) + const rowsToInsert = data.rows.map((rowData, i) => ({ + id: `row_${generateId().replace(/-/g, '')}`, + tableId: data.tableId, + workspaceId: data.workspaceId, + data: rowData, + position: i, + orderKey: orderKeys[i], + createdAt: now, + updatedAt: now, + ...(data.userId ? { createdBy: data.userId } : {}), + })) + + const batchSize = TABLE_LIMITS.MAX_BATCH_INSERT_SIZE + for (let i = 0; i < rowsToInsert.length; i += batchSize) { + const chunk = rowsToInsert.slice(i, i + batchSize) + const inserted = await trx.insert(userTableRows).values(chunk).returning({ + id: userTableRows.id, + }) + insertedCount += inserted.length + } + } + + logger.info( + `[${requestId}] Replaced rows in table ${data.tableId}: deleted ${deletedRows.length}, inserted ${insertedCount}` + ) + + return { deletedCount: deletedRows.length, insertedCount } +} + +/** + * Upserts a row: updates an existing row if a match is found on the conflict target + * column, otherwise inserts a new row. + * + * Uses a single unique column for matching (not OR across all unique columns) to avoid + * ambiguous matches when multiple unique columns exist. Capacity enforcement lives + * in the `increment_user_table_row_count` trigger (migration 0198). On the insert + * path we acquire the per-table advisory lock and re-check for an existing match + * before inserting, so a concurrent upsert racing on the same conflict target + * cannot produce a duplicate row. + * + * @param data - Upsert data including optional conflictTarget + * @param table - Table definition + * @param requestId - Request ID for logging + * @returns The upserted row and whether it was an insert or update + * @throws Error if no unique columns, ambiguous conflict target, or capacity exceeded + */ +export async function upsertRow( + data: UpsertRowData, + table: TableDefinition, + requestId: string +): Promise { + const schema = table.schema + const uniqueColumns = getUniqueColumns(schema) + + if (uniqueColumns.length === 0) { + throw new Error( + 'Upsert requires at least one unique column in the schema. Please add a unique constraint to a column or use insert instead.' + ) + } + + // Determine the single conflict target column, resolving to its stable + // storage id (the row-data key). `conflictTarget` may arrive as an id + // (first-party) or a name (legacy/internal) — match either. + let targetColumnKey: string + if (data.conflictTarget) { + const col = uniqueColumns.find( + (c) => getColumnId(c) === data.conflictTarget || c.name === data.conflictTarget + ) + if (!col) { + throw new Error( + `Column "${data.conflictTarget}" is not a unique column. Available unique columns: ${uniqueColumns.map((c) => c.name).join(', ')}` + ) + } + targetColumnKey = getColumnId(col) + } else if (uniqueColumns.length === 1) { + targetColumnKey = getColumnId(uniqueColumns[0]) + } else { + throw new Error( + `Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify conflictTarget to indicate which column to match on.` + ) + } + + // Validate row data + const sizeValidation = validateRowSize(data.data) + if (!sizeValidation.valid) { + throw new Error(sizeValidation.errors.join(', ')) + } + + const schemaValidation = coerceRowToSchema(data.data, schema) + if (!schemaValidation.valid) { + throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) + } + + // Read the conflict-target value *after* coercion so `matchFilter` branches on + // the persisted type (e.g. a coerced `"123"` → `123` matches existing rows). + const targetValue = data.data[targetColumnKey] + if (targetValue === undefined || targetValue === null) { + // Surface the display name, not the internal id — v1 callers pass a name. + const targetColumnName = + uniqueColumns.find((c) => getColumnId(c) === targetColumnKey)?.name ?? targetColumnKey + throw new Error(`Upsert requires a value for the conflict target column "${targetColumnName}"`) + } + + // `data->` and `data->>` accept the JSON key as a parameterized text value; + // no need for `sql.raw` interpolation. + const matchFilter = + typeof targetValue === 'string' + ? sql`${userTableRows.data}->>${targetColumnKey}::text = ${String(targetValue)}` + : sql`(${userTableRows.data}->${targetColumnKey}::text)::jsonb = ${JSON.stringify(targetValue)}::jsonb` + + // Capacity enforcement for the insert path lives in the `increment_user_table_row_count` + // trigger (migration 0198). The update path doesn't change row_count, so no check needed. + const result = await db.transaction(async (trx) => { + await setTableTxTimeouts(trx) + // The conflict lookups below match on `data->>key` — unestimatable, and an + // insert-path upsert (no existing match) can't exit early, so the planner + // would seq-scan the whole shared relation. See withSeqscanOff. + await trx.execute(sql`SET LOCAL enable_seqscan = off`) + + // Find existing row by single conflict target column + const [existingRow] = await trx + .select() + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, data.tableId), + eq(userTableRows.workspaceId, data.workspaceId), + matchFilter + ) + ) + .limit(1) + + // Check uniqueness on ALL unique columns (not just the conflict target) + const uniqueValidation = await checkUniqueConstraintsDb( + data.tableId, + data.data, + schema, + existingRow?.id, // exclude the matched row on updates + trx + ) + if (!uniqueValidation.valid) { + throw new Error(`Unique constraint violation: ${uniqueValidation.errors.join(', ')}`) + } + + const now = new Date() + + // Resolve which row (if any) we should update. If the initial SELECT missed, + // acquire the lock and re-check — a concurrent upsert may have inserted the + // matching row between our SELECT and the INSERT path; without the re-check + // both transactions would insert and bypass the app-level unique check. + let matchedRowId = existingRow?.id + let previousData = existingRow?.data as RowData | undefined + if (!matchedRowId) { + await acquireRowOrderLock(trx, data.tableId) + const [racedRow] = await trx + .select({ id: userTableRows.id, data: userTableRows.data }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, data.tableId), + eq(userTableRows.workspaceId, data.workspaceId), + matchFilter + ) + ) + .limit(1) + if (racedRow) { + matchedRowId = racedRow.id + previousData = racedRow.data as RowData + } + } + + if (matchedRowId) { + const [updatedRow] = await trx + .update(userTableRows) + .set({ data: data.data, updatedAt: now }) + .where(eq(userTableRows.id, matchedRowId)) + .returning() + + const executions = await loadExecutionsForRow(trx, updatedRow.id) + return { + row: { + id: updatedRow.id, + data: updatedRow.data as RowData, + executions, + position: updatedRow.position, + orderKey: updatedRow.orderKey ?? undefined, + createdAt: updatedRow.createdAt, + updatedAt: updatedRow.updatedAt, + }, + previousData, + operation: 'update' as const, + } + } + + const [insertedRow] = await trx + .insert(userTableRows) + .values({ + id: `row_${generateId().replace(/-/g, '')}`, + tableId: data.tableId, + workspaceId: data.workspaceId, + data: data.data, + position: await reserveInsertPosition(trx, data.tableId), + orderKey: await resolveInsertOrderKey(trx, data.tableId), + createdAt: now, + updatedAt: now, + ...(data.userId ? { createdBy: data.userId } : {}), + }) + .returning() + + return { + row: { + id: insertedRow.id, + data: insertedRow.data as RowData, + executions: {}, + position: insertedRow.position, + orderKey: insertedRow.orderKey ?? undefined, + createdAt: insertedRow.createdAt, + updatedAt: insertedRow.updatedAt, + }, + operation: 'insert' as const, + } + }) + + logger.info( + `[${requestId}] Upserted (${result.operation}) row ${result.row.id} in table ${data.tableId}` + ) + + if (result.operation === 'insert') { + void fireTableTrigger( + data.tableId, + table.name, + 'insert', + [result.row], + null, + table.schema, + requestId + ) + } else if (result.operation === 'update' && result.previousData) { + const oldRows = new Map([[result.row.id, result.previousData]]) + void fireTableTrigger( + data.tableId, + table.name, + 'update', + [result.row], + oldRows, + table.schema, + requestId + ) + } + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: [result.row.id], + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: data.userId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (upsertRow) failed:`, err)) + + return result +} + +/** + * Canonical ORDER BY for a table's rows, shared by `queryRows` (the paginated + * list) and `findRowMatches` so a match's ordinal lines up with its index in + * the list. Order: explicit data sort (if any) → fractional `order_key` or + * legacy `position` → `id`. The `id` tiebreak is always appended so equal + * positions order deterministically — without it two separate query executions + * (a find vs a list page) could shuffle ties and misalign ordinals. + */ +function buildRowOrderBySql( + sort: Sort | undefined, + tableName: string, + columns: ColumnDefinition[] +): SQL { + const primary = isTablesFractionalOrderingEnabled + ? `${tableName}.order_key` + : `${tableName}.position` + const id = `${tableName}.id` + if (sort && Object.keys(sort).length > 0) { + const sortClause = buildSortClause(sort, tableName, columns) + if (sortClause) { + return sql.join([sortClause, sql.raw(primary), sql.raw(id)], sql.raw(', ')) + } + } + return sql.raw(`${primary}, ${id}`) +} + +/** One matching cell from {@link findRowMatches}. */ +export interface FindRowMatch { + /** 0-based index of the row in the filtered+sorted view (aligns with the list query). */ + ordinal: number + rowId: string + /** Stable column id of the matching cell (the JSONB storage key), not the display name. */ + column: string +} + +/** Max matching cells returned by {@link findRowMatches}; one extra is fetched to detect truncation. */ +const FIND_MATCH_LIMIT = 1000 + +/** + * Case-insensitive substring search across every cell of a table's rows. Each + * matching cell becomes a {@link FindRowMatch} carrying its row id, column, and + * 0-based ordinal in the filtered+sorted view (so the client can page up to and + * reveal it). `filter`/`sort` mirror the active list view via + * {@link buildRowOrderBySql}, keeping ordinals aligned. + * + * Cost: one pass over the table's rows — `ILIKE` over `jsonb_each_text` cannot + * use the JSONB GIN index, and the ordinal's `row_number()` needs every row + * counted regardless. The planner can't estimate the lateral ILIKE (jsonb is + * opaque to it), so left alone it seq-scans the entire shared relation and + * disk-sorts the window input (measured 75s on a 1M-row table in a 12M-row + * relation). `SET LOCAL` planner flags keep it tenant-bounded; on the default + * order they additionally force the streaming `(table_id, order_key, id)` index + * walk where `row_number()` needs no sort at all (measured 2s). A `pg_trgm` GIN + * index on a text projection is the future accelerator if needed. + */ +export async function findRowMatches( + table: TableDefinition, + options: { q: string; filter?: Filter; sort?: Sort }, + requestId: string +): Promise<{ matches: FindRowMatch[]; truncated: boolean }> { + const tableName = USER_TABLE_ROWS_SQL_NAME + const columns = table.schema.columns + // Row data is keyed by stable column id, so scan/return JSONB keys as ids. + const columnIds = columns.map(getColumnId) + if (columnIds.length === 0) return { matches: [], truncated: false } + + // Same visibility rule as queryRows: don't surface rows a running delete job will remove. + const deleteMask = await pendingDeleteMask(table) + + const baseConditions = and( + eq(userTableRows.tableId, table.id), + eq(userTableRows.workspaceId, table.workspaceId), + deleteMask + ) + let whereClause: SQL | undefined = baseConditions + if (options.filter && Object.keys(options.filter).length > 0) { + const filterClause = buildFilterClause(options.filter, tableName, columns) + if (filterClause) whereClause = and(baseConditions, filterClause) + } + + const orderBySql = buildRowOrderBySql(options.sort, tableName, columns) + const pattern = `%${escapeLikePattern(options.q)}%` + + const result = await db.transaction(async (trx) => { + // Planner flags, not correctness: `enable_* = off` only penalizes a plan shape, so a + // genuinely required sort still runs. Seqscan off keeps the scan inside the tenant's rows + // (the lateral ILIKE is unestimatable, so the planner otherwise walks the whole shared + // relation). On the default order, the remaining flags steer to the already-sorted + // `(table_id, order_key, id)` index walk so the window function streams without a 100MB+ + // disk sort; a custom sort has no index to stream from, so those flags would only distort + // that plan. + await trx.execute(sql`SET LOCAL enable_seqscan = off`) + if (!options.sort) { + await trx.execute(sql`SET LOCAL enable_bitmapscan = off`) + await trx.execute(sql`SET LOCAL enable_sort = off`) + await trx.execute(sql`SET LOCAL max_parallel_workers_per_gather = 0`) + } + return trx.execute<{ + ordinal: string | number + id: string + column_name: string + }>(sql` + WITH ordered AS ( + SELECT id, data, row_number() OVER (ORDER BY ${orderBySql}) - 1 AS ordinal + FROM ${userTableRows} + WHERE ${whereClause} + ) + SELECT o.ordinal, o.id, kv.key AS column_name + FROM ordered o + CROSS JOIN LATERAL jsonb_each_text(o.data) kv + WHERE kv.value ILIKE ${pattern} + AND ${inArray(sql`kv.key`, columnIds)} + ORDER BY o.ordinal + LIMIT ${FIND_MATCH_LIMIT + 1} + `) + }) + + const all = Array.from(result) + const truncated = all.length > FIND_MATCH_LIMIT + const sliced = truncated ? all.slice(0, FIND_MATCH_LIMIT) : all + const matches: FindRowMatch[] = sliced.map((r) => ({ + ordinal: Number(r.ordinal), + rowId: r.id, + column: r.column_name, + })) + + logger.info( + `[${requestId}] Find "${options.q}" in table ${table.id}: ${matches.length} match(es)${truncated ? ' (truncated)' : ''}` + ) + + return { matches, truncated } +} + +/** + * Queries rows from a table with filtering, sorting, and pagination. + * + * Filter cost model: equality filters (`$eq`, `$in`) compile to JSONB + * containment (`@>`) and hit the GIN (jsonb_path_ops) index on + * `user_table_rows.data`. Range operators (`$gt`, `$gte`, `$lt`, `$lte`) and + * `$contains` compile to `data->>'field'` text extraction and bypass the GIN + * index — they fall back to a sequential scan of the rows for the table + * (bounded only by the btree on `table_id`). Prefer equality on hot paths; set + * `includeTotal: false` when the caller does not need the `COUNT(*)`. + * + * @param table - Table definition (provides id, workspaceId, and column schema for type-aware filter/sort casts) + * @param options - Query options (filter, sort, limit, offset) + * @param requestId - Request ID for logging + * @returns Query result with rows and pagination info + */ +/** + * Visibility mask for a running delete job: returns a clause keeping only rows the job will NOT + * delete, or `undefined` when no delete job is running. The job's persisted scope + * ({@link TableDeleteJobPayload}) defines the doomed set — `matches(filter) AND created_at <= + * cutoff AND id NOT IN excludeRowIds` — exactly what the worker's `selectRowIdPage` selects, so + * mid-job reads (refresh, other clients, exports) are consistent with the eventual result. The + * mask lifts automatically when the job leaves `running` (done, failed, or canceled). + * + * `(doomed) IS NOT TRUE` rather than `NOT (doomed)`: JSONB predicates evaluate to NULL on missing + * cells, and those rows are NOT selected for deletion (NULL ≠ TRUE) — they must stay visible. + */ +export async function pendingDeleteMask(table: TableDefinition): Promise { + const [job] = await db + .select({ payload: tableJobs.payload }) + .from(tableJobs) + .where( + and( + eq(tableJobs.tableId, table.id), + eq(tableJobs.status, 'running'), + eq(tableJobs.type, 'delete') + ) + ) + .limit(1) + if (!job?.payload) return undefined + const scope = job.payload as TableDeleteJobPayload + + const doomedParts: SQL[] = [] + if (scope.filter && Object.keys(scope.filter).length > 0) { + try { + const clause = buildFilterClause(scope.filter, USER_TABLE_ROWS_SQL_NAME, table.schema.columns) + if (clause) doomedParts.push(clause) + } catch (error) { + // Schema drifted mid-job (column renamed/deleted). Showing doomed rows briefly beats + // failing every read; the worker resolves the same way on its next page. + logger.warn(`Skipping delete-job mask for table ${table.id}: stale filter`, { + error: toError(error).message, + }) + return undefined + } + } + if (scope.cutoff) doomedParts.push(lte(userTableRows.createdAt, new Date(scope.cutoff))) + if (scope.excludeRowIds && scope.excludeRowIds.length > 0) { + doomedParts.push(notInArray(userTableRows.id, scope.excludeRowIds)) + } + if (doomedParts.length === 0) return undefined + return sql`(${and(...doomedParts)}) IS NOT TRUE` +} + +/** + * `COUNT(*)` for a filtered view, kept inside the tenant's rows: measured + * 12.7s → 1.0s counting a rare ILIKE filter on a 1M-row table inside a 12M-row + * relation (see {@link withSeqscanOff} for why the planner gets this wrong). + */ +async function countRowsTenantBounded(whereClause: SQL | undefined): Promise { + return withSeqscanOff(async (trx) => { + const [result] = await trx.select({ count: count() }).from(userTableRows).where(whereClause) + return Number(result.count) + }) +} + +export async function queryRows( + table: TableDefinition, + options: QueryOptions, + requestId: string +): Promise { + const { + filter, + sort, + limit = TABLE_LIMITS.DEFAULT_QUERY_LIMIT, + offset = 0, + after, + includeTotal = true, + withExecutions = true, + } = options + + const tableName = USER_TABLE_ROWS_SQL_NAME + const columns = table.schema.columns + + // Hide rows a running delete job is about to remove — both the page and the count below share + // this clause, so totals stay consistent with the visible rows. + const deleteMask = await pendingDeleteMask(table) + + const baseConditions = and( + eq(userTableRows.tableId, table.id), + eq(userTableRows.workspaceId, table.workspaceId), + deleteMask + ) + + let whereClause = baseConditions + if (filter && Object.keys(filter).length > 0) { + const filterClause = buildFilterClause(filter, tableName, columns) + if (filterClause) { + whereClause = and(baseConditions, filterClause) + } + } + + // Keyset page: seek past the cursor on the default `(order_key, id)` order instead of paying + // OFFSET's scan-and-discard of every prior row (O(N²) across a deep scroll / full drain). Only + // valid without a custom sort — the contract rejects `after` + `sort` together. The count below + // deliberately excludes the cursor: totals cover the whole view, not the remaining pages. + const pageWhere = + after && !sort + ? and( + whereClause, + sql`(${userTableRows.orderKey}, ${userTableRows.id}) > (${after.orderKey}, ${after.id})` + ) + : whereClause + + const buildPageQuery = (executor: DbExecutor) => { + const query = executor + .select() + .from(userTableRows) + .where(pageWhere ?? baseConditions) + .orderBy(buildRowOrderBySql(sort, tableName, columns)) + return after ? query.limit(limit) : query.limit(limit).offset(offset) + } + + // Count and page fetch are independent reads — run them concurrently so the + // `includeTotal` hot path doesn't pay two serial round-trips. Filtered counts + // go through the tenant-bounded variant (see countRowsTenantBounded); the + // unfiltered count already plans an index-only scan on the table_id prefix. + // Custom column sorts order by `data->>'col'` — unestimatable, so left alone + // the planner seq-scans and sorts the whole shared relation on every page + // (9.7s measured on a 1M-row table; 0.76s tenant-bounded). Default-order + // pages already stream the `(table_id, order_key, id)` index. + const hasFilter = Boolean(filter && Object.keys(filter).length > 0) + const rowsPromise = sort ? withSeqscanOff(async (trx) => buildPageQuery(trx)) : buildPageQuery(db) + const countPromise = includeTotal + ? hasFilter + ? countRowsTenantBounded(whereClause) + : db + .select({ count: count() }) + .from(userTableRows) + .where(whereClause ?? baseConditions) + .then((r) => Number(r[0].count)) + : null + + const [rows, totalCount] = await Promise.all([rowsPromise, countPromise]) + + const executionsByRow = withExecutions + ? await loadExecutionsByRow( + db, + rows.map((r) => r.id) + ) + : null + + logger.info( + `[${requestId}] Queried ${rows.length} rows from table ${table.id} (total: ${totalCount})` + ) + + return { + rows: rows.map((r) => ({ + id: r.id, + data: r.data as RowData, + executions: executionsByRow?.get(r.id) ?? {}, + position: r.position, + orderKey: r.orderKey ?? undefined, + createdAt: r.createdAt, + updatedAt: r.updatedAt, + })), + rowCount: rows.length, + totalCount, + limit, + offset, + } +} + +/** + * Gets a single row by ID. + * + * @param tableId - Table ID + * @param rowId - Row ID to fetch + * @param workspaceId - Workspace ID for access control + * @returns Row or null if not found + */ +export async function getRowById( + tableId: string, + rowId: string, + workspaceId: string +): Promise { + const results = await db + .select() + .from(userTableRows) + .where( + and( + eq(userTableRows.id, rowId), + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId) + ) + ) + .limit(1) + + if (results.length === 0) return null + + const row = results[0] + const executions = await loadExecutionsForRow(db, row.id) + return { + id: row.id, + data: row.data as RowData, + executions, + position: row.position, + orderKey: row.orderKey ?? undefined, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } +} + +/** Internal: thrown inside `db.transaction` to roll back when the executions + * guard rejects a write. The outer `.catch` translates it into a `null` return. */ +class GuardRejected extends Error { + constructor() { + super('cell-write guard rejected') + } +} + +/** + * Updates a single row. + * + * @param data - Update data + * @param table - Table definition + * @param requestId - Request ID for logging + * @returns Updated row + * @throws Error if row not found or validation fails + */ +export async function updateRow( + data: UpdateRowData, + table: TableDefinition, + requestId: string +): Promise { + // Get existing row + const existingRow = await getRowById(data.tableId, data.rowId, data.workspaceId) + if (!existingRow) { + throw new Error('Row not found') + } + + // Merge partial update with existing row data so callers can pass only changed fields + const mergedData = { + ...(existingRow.data as RowData), + ...data.data, + } + // Auto-clear exec records for workflow output columns the user just wiped + // AND for downstream groups whose deps just changed. Surfaces the in-flight + // downstream groups so the caller can cancel + re-run them. + const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = + deriveExecClearsForDataPatch( + data.data, + table.schema, + existingRow.executions, + data.executionsPatch, + mergedData + ) + const mergedExecutions = applyExecutionsPatch(existingRow.executions, effectiveExecutionsPatch) + + // Validate size + const sizeValidation = validateRowSize(mergedData) + if (!sizeValidation.valid) { + throw new Error(sizeValidation.errors.join(', ')) + } + + // Validate against schema + const schemaValidation = coerceRowToSchema(mergedData, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) + } + + // Check unique constraints using optimized database query + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0) { + const uniqueValidation = await checkUniqueConstraintsDb( + data.tableId, + mergedData, + table.schema, + data.rowId // Exclude current row + ) + if (!uniqueValidation.valid) { + throw new Error(uniqueValidation.errors.join(', ')) + } + } + + const now = new Date() + + // Cell-task partial writes pass `cancellationGuard` so the upsert into + // `tableRowExecutions` is a no-op when (a) a stop click already wrote + // `cancelled` for this run, or (b) a newer run has taken over the cell + // with a different executionId. Authoritative cancel writes from + // `cancelWorkflowGroupRuns` skip the guard entirely. Data + executions + // commit in one transaction so a partial write can't leave the sidecar + // and the row out of sync. + const guard = data.cancellationGuard + const guardRejected = await db + .transaction(async (trx) => { + await trx + .update(userTableRows) + .set({ data: mergedData, updatedAt: now }) + .where(eq(userTableRows.id, data.rowId)) + + const result = await writeExecutionsPatch( + trx, + data.tableId, + data.rowId, + effectiveExecutionsPatch, + guard + ) + if (result === 'guard-rejected') { + // Roll back the data update too — the worker isn't authoritative. + throw new GuardRejected() + } + return false + }) + .catch((err) => { + if (err instanceof GuardRejected) return true + throw err + }) + + if (guardRejected) { + return null + } + + logger.info(`[${requestId}] Updated row ${data.rowId} in table ${data.tableId}`) + + const updatedRow: TableRow = { + id: data.rowId, + data: mergedData, + executions: mergedExecutions, + position: existingRow.position, + createdAt: existingRow.createdAt, + updatedAt: now, + } + + const oldRows = new Map([[data.rowId, existingRow.data as RowData]]) + void fireTableTrigger( + data.tableId, + table.name, + 'update', + [updatedRow], + oldRows, + table.schema, + requestId + ) + + // Auto-fire only on user-facing data edits. Internal callers that mutate + // executions (cell-task partial/terminal writes, cancel writes) always pass + // `executionsPatch` — re-dispatching from those would recursively spawn new + // dispatches for every running/terminal write, flooding the dispatcher with + // redundant pre-stamps that strand `pending` cells. + const isInternalExecWrite = data.executionsPatch && Object.keys(data.executionsPatch).length > 0 + if (isInternalExecWrite) { + return updatedRow + } + + // Two passes: + // 1. Cancel in-flight downstream groups whose dep just changed, then + // manually re-run them — the cancel writes `cancelled` per cell and + // `mode: 'incomplete' + isManualRun: true` wipes those entries and + // re-enqueues. + // 2. `mode: 'new'` for groups that just had their exec entries cleared + // (own-output wipe OR terminal downstream dep-changed) — the + // dispatcher's `jsonb_exists_all` SQL filter lets the row through + // because at least one targeted group's exec is now missing. + if (inFlightDownstreamGroups.length > 0) { + void (async () => { + try { + await cancelWorkflowGroupRuns(data.tableId, data.rowId, { + groupIds: inFlightDownstreamGroups, + }) + await runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + mode: 'incomplete', + isManualRun: true, + rowIds: [data.rowId], + groupIds: inFlightDownstreamGroups, + requestId, + triggeredByUserId: data.actorUserId, + }) + } catch (err) { + logger.error(`[${requestId}] cancel+rerun for in-flight downstream groups failed:`, err) + } + })() + } + void runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + rowIds: [data.rowId], + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: data.actorUserId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRow) failed:`, err)) + + return updatedRow +} + +/** + * Deletes a single row (hard delete). + * + * @param tableId - Table ID + * @param rowId - Row ID to delete + * @param workspaceId - Workspace ID for access control + * @param requestId - Request ID for logging + * @throws Error if row not found + */ +export async function deleteRow( + tableId: string, + rowId: string, + workspaceId: string, + requestId: string +): Promise { + const deleted = await deleteOrderedRow({ tableId, rowId, workspaceId }) + if (!deleted) throw new Error('Row not found') + + logger.info(`[${requestId}] Deleted row ${rowId} from table ${tableId}`) +} + +/** + * Updates multiple rows matching a filter. + * + * @param table - Table definition (provides column schema for type-aware filter casts) + * @param data - Bulk update data + * @param requestId - Request ID for logging + * @returns Bulk operation result + */ +export async function updateRowsByFilter( + table: TableDefinition, + data: BulkUpdateData, + requestId: string +): Promise { + const tableName = USER_TABLE_ROWS_SQL_NAME + + const filterClause = buildFilterClause(data.filter, tableName, table.schema.columns) + if (!filterClause) { + throw new Error('Filter is required for bulk update') + } + + const baseConditions = and( + eq(userTableRows.tableId, table.id), + eq(userTableRows.workspaceId, table.workspaceId) + ) + + // Tenant-bounded: the jsonb filter is unestimatable and otherwise sends the planner to a + // whole-shared-relation seq scan (14.4s measured on a 1M-row table). + const matchingRows = await withSeqscanOff(async (trx) => { + let query = trx + .select({ id: userTableRows.id, data: userTableRows.data }) + .from(userTableRows) + .where(and(baseConditions, filterClause)) + if (data.limit) { + query = query.limit(data.limit) as typeof query + } + return query + }) + + if (matchingRows.length === 0) { + return { affectedCount: 0, affectedRowIds: [] } + } + + // Coerce the patch itself in place — the write below persists `data.data` + // (as `patchJson`), so coercing only the per-row merged copies would be + // discarded. The merged validation in the loop still enforces required + // fields against the full row. + coerceRowValues(data.data, table.schema) + + for (const row of matchingRows) { + const existingData = row.data as RowData + const mergedData = { ...existingData, ...data.data } + + const sizeValidation = validateRowSize(mergedData) + if (!sizeValidation.valid) { + throw new Error(`Row ${row.id}: ${sizeValidation.errors.join(', ')}`) + } + + const schemaValidation = coerceRowToSchema(mergedData, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${row.id}: ${schemaValidation.errors.join(', ')}`) + } + } + + const uniqueColumns = getUniqueColumns(table.schema) + const uniqueColumnsInUpdate = uniqueColumns.filter((col) => col.name in data.data) + if (uniqueColumnsInUpdate.length > 0) { + if (matchingRows.length > 1) { + throw new Error( + `Cannot set unique column values when updating multiple rows. ` + + `Columns with unique constraint: ${uniqueColumnsInUpdate.map((c) => c.name).join(', ')}. ` + + `Updating ${matchingRows.length} rows with the same value would violate uniqueness.` + ) + } + + // Only one row — only the touched unique columns need re-checking. + const row = matchingRows[0] + const mergedData = { ...(row.data as RowData), ...data.data } + const uniqueValidation = await checkUniqueConstraintsDb( + table.id, + mergedData, + table.schema, + row.id + ) + if (!uniqueValidation.valid) { + throw new Error(`Unique constraint violation: ${uniqueValidation.errors.join(', ')}`) + } + } + + const now = new Date() + const ids = matchingRows.map((r) => r.id) + const patchJson = JSON.stringify(data.data) + + await db.transaction(async (trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + for (let i = 0; i < ids.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { + const batchIds = ids.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) + await trx + .update(userTableRows) + .set({ + data: sql`${userTableRows.data} || ${patchJson}::jsonb`, + updatedAt: now, + }) + .where(inArray(userTableRows.id, batchIds)) + } + }) + + logger.info(`[${requestId}] Updated ${matchingRows.length} rows in table ${table.id}`) + + const oldRows = new Map(matchingRows.map((r) => [r.id, r.data as RowData])) + const updatedRows: TableRow[] = matchingRows.map((r) => ({ + id: r.id, + data: { ...(r.data as RowData), ...data.data }, + executions: {}, + position: 0, + createdAt: now, + updatedAt: now, + })) + void fireTableTrigger( + table.id, + table.name, + 'update', + updatedRows, + oldRows, + table.schema, + requestId + ) + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: updatedRows.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: data.actorUserId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRowsByFilter) failed:`, err)) + + return { + affectedCount: matchingRows.length, + affectedRowIds: ids, + } +} + +/** + * Updates multiple rows with per-row data in a single transaction. + * Avoids the race condition of parallel update_row calls overwriting each other. + */ +export async function batchUpdateRows( + data: BatchUpdateByIdData, + table: TableDefinition, + requestId: string +): Promise { + if (data.updates.length === 0) { + return { affectedCount: 0, affectedRowIds: [] } + } + + const rowIds = data.updates.map((u) => u.rowId) + const existingRows = await db + .select({ + id: userTableRows.id, + data: userTableRows.data, + }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, data.tableId), + eq(userTableRows.workspaceId, data.workspaceId), + inArray(userTableRows.id, rowIds) + ) + ) + + const executionsByRow = await loadExecutionsByRow( + db, + existingRows.map((r) => r.id) + ) + + type ExistingRow = { data: RowData; executions: RowExecutions } + const existingMap = new Map( + existingRows.map((r) => [ + r.id, + { data: r.data as RowData, executions: executionsByRow.get(r.id) ?? {} }, + ]) + ) + + const missing = rowIds.filter((id) => !existingMap.has(id)) + if (missing.length > 0) { + throw new Error(`Rows not found: ${missing.join(', ')}`) + } + + const mergedUpdates: Array<{ + rowId: string + mergedData: RowData + mergedExecutions: RowExecutions + executionsPatch?: Record + inFlightDownstreamGroups: string[] + }> = [] + for (const update of data.updates) { + const existing = existingMap.get(update.rowId)! + const merged = { ...existing.data, ...update.data } + // Auto-clear exec records for workflow output columns the user just + // wiped AND downstream dep-changed terminal groups — same rationale as + // `updateRow`. Per-row in-flight downstream groups are surfaced so we + // can run the cancel+rerun orchestration after the batch commits. + const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = + deriveExecClearsForDataPatch( + update.data, + table.schema, + existing.executions, + update.executionsPatch, + merged + ) + const mergedExecutions = applyExecutionsPatch(existing.executions, effectiveExecutionsPatch) + + const sizeValidation = validateRowSize(merged) + if (!sizeValidation.valid) { + throw new Error(`Row ${update.rowId}: ${sizeValidation.errors.join(', ')}`) + } + + const schemaValidation = coerceRowToSchema(merged, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${update.rowId}: ${schemaValidation.errors.join(', ')}`) + } + + mergedUpdates.push({ + rowId: update.rowId, + mergedData: merged, + mergedExecutions, + executionsPatch: effectiveExecutionsPatch, + inFlightDownstreamGroups, + }) + } + + const uniqueColumns = getUniqueColumns(table.schema) + if (uniqueColumns.length > 0) { + for (const { rowId, mergedData } of mergedUpdates) { + const uniqueValidation = await checkUniqueConstraintsDb( + data.tableId, + mergedData, + table.schema, + rowId + ) + if (!uniqueValidation.valid) { + throw new Error(`Row ${rowId}: ${uniqueValidation.errors.join(', ')}`) + } + } + } + + const now = new Date() + + await db.transaction(async (trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + for (let i = 0; i < mergedUpdates.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { + const batch = mergedUpdates.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) + // Update row data in parallel; sidecar exec writes are sequential per + // row (each goes through writeExecutionsPatch's per-key upsert). + const dataPromises = batch.map(({ rowId, mergedData }) => + trx + .update(userTableRows) + .set({ data: mergedData, updatedAt: now }) + .where(eq(userTableRows.id, rowId)) + ) + await Promise.all(dataPromises) + for (const { rowId, executionsPatch } of batch) { + await writeExecutionsPatch(trx, data.tableId, rowId, executionsPatch) + } + } + }) + + logger.info(`[${requestId}] Batch updated ${mergedUpdates.length} rows in table ${data.tableId}`) + + const oldRowsForTrigger = new Map( + data.updates.map((u) => [u.rowId, existingMap.get(u.rowId)!.data]) + ) + const updatedRowsForTrigger: TableRow[] = mergedUpdates.map( + ({ rowId, mergedData, mergedExecutions }) => ({ + id: rowId, + data: mergedData, + executions: mergedExecutions, + position: 0, + createdAt: now, + updatedAt: now, + }) + ) + void fireTableTrigger( + data.tableId, + table.name, + 'update', + updatedRowsForTrigger, + oldRowsForTrigger, + table.schema, + requestId + ) + // Per-row cancel+rerun for in-flight downstream groups whose deps just + // changed — same orchestration as single-row `updateRow`. Without this, + // batch updates would leave running workflows reading stale dep values. + // Each row needs its own cancel + manual-incomplete dispatch because + // `cancelWorkflowGroupRuns`'s `groupIds` filter is per-row. + const rowsWithInFlightDownstream = mergedUpdates.filter( + (u) => u.inFlightDownstreamGroups.length > 0 + ) + if (rowsWithInFlightDownstream.length > 0) { + void (async () => { + try { + for (const { rowId, inFlightDownstreamGroups } of rowsWithInFlightDownstream) { + await cancelWorkflowGroupRuns(data.tableId, rowId, { + groupIds: inFlightDownstreamGroups, + }) + await runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + mode: 'incomplete', + isManualRun: true, + rowIds: [rowId], + groupIds: inFlightDownstreamGroups, + requestId, + triggeredByUserId: data.actorUserId, + }) + } + } catch (err) { + logger.error( + `[${requestId}] cancel+rerun for in-flight downstream groups (batch) failed:`, + err + ) + } + })() + } + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: updatedRowsForTrigger.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + triggeredByUserId: data.actorUserId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchUpdateRows) failed:`, err)) + + return { + affectedCount: mergedUpdates.length, + affectedRowIds: mergedUpdates.map((u) => u.rowId), + } +} + +/** + * Deletes multiple rows matching a filter. + * + * @param table - Table definition (provides column schema for type-aware filter casts) + * @param data - Bulk delete data + * @param requestId - Request ID for logging + * @returns Bulk operation result + */ +export async function deleteRowsByFilter( + table: TableDefinition, + data: BulkDeleteData, + requestId: string +): Promise { + const tableName = USER_TABLE_ROWS_SQL_NAME + + // Build filter clause + const filterClause = buildFilterClause(data.filter, tableName, table.schema.columns) + if (!filterClause) { + throw new Error('Filter is required for bulk delete') + } + + // Find matching rows + const baseConditions = and( + eq(userTableRows.tableId, table.id), + eq(userTableRows.workspaceId, table.workspaceId) + ) + + // Tenant-bounded for the same reason as updateRowsByFilter — see withSeqscanOff. + const matchingRows = await withSeqscanOff(async (trx) => { + let query = trx + .select({ id: userTableRows.id, position: userTableRows.position }) + .from(userTableRows) + .where(and(baseConditions, filterClause)) + if (data.limit) { + query = query.limit(data.limit) as typeof query + } + return query + }) + + if (matchingRows.length === 0) { + return { affectedCount: 0, affectedRowIds: [] } + } + + const rowIds = matchingRows.map((r) => r.id) + + await deleteOrderedRowsByIds({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds, + }) + + logger.info(`[${requestId}] Deleted ${matchingRows.length} rows from table ${table.id}`) + + return { + affectedCount: matchingRows.length, + affectedRowIds: rowIds, + } +} + +/** + * Deletes rows by their IDs. + * + * @param data - Row IDs and table context + * @param requestId - Request ID for logging + * @returns Deletion result with deleted/missing row IDs + */ +export async function deleteRowsByIds( + data: BulkDeleteByIdsData, + requestId: string +): Promise { + const uniqueRequestedRowIds = Array.from(new Set(data.rowIds)) + + const deletedRows = await deleteOrderedRowsByIds({ + tableId: data.tableId, + workspaceId: data.workspaceId, + rowIds: uniqueRequestedRowIds, + }) + + const deletedIds = deletedRows.map((r) => r.id) + const deletedIdSet = new Set(deletedIds) + const missingRowIds = uniqueRequestedRowIds.filter((id) => !deletedIdSet.has(id)) + + logger.info(`[${requestId}] Deleted ${deletedIds.length} rows by ID from table ${data.tableId}`) + + return { + deletedCount: deletedIds.length, + deletedRowIds: deletedIds, + requestedCount: uniqueRequestedRowIds.length, + missingRowIds, + } +} diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 1037f6c6526..923cc97e0c3 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -8,101 +8,27 @@ */ import { db } from '@sim/db' -import { tableJobs, tableRowExecutions, userTableDefinitions, userTableRows } from '@sim/db/schema' +import { tableJobs, userTableDefinitions, userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { getPostgresErrorCode, toError } from '@sim/utils/errors' +import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { - and, - asc, - count, - desc, - eq, - gt, - gte, - inArray, - isNull, - lt, - lte, - ne, - notInArray, - or, - type SQL, - sql, -} from 'drizzle-orm' -import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/feature-flags' +import { and, count, eq, isNull, sql } from 'drizzle-orm' import { generateRestoreName } from '@/lib/core/utils/restore-name' import type { DbOrTx } from '@/lib/db/types' -import { - columnMatchesRef, - generateColumnId, - getColumnId, - remapGroupColumnRefs, - withGeneratedColumnIds, -} from './column-keys' -import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from './constants' -import { areGroupDepsSatisfied } from './deps' -import { CSV_MAX_BATCH_SIZE } from './import' -import { keyBetween, nKeysBetween } from './order-key' -import { type DbExecutor, type DbTransaction, withSeqscanOff } from './planner' -import { buildFilterClause, buildSortClause, escapeLikePattern } from './sql' -import { fireTableTrigger } from './trigger' +import { generateColumnId, getColumnId, withGeneratedColumnIds } from '@/lib/table/column-keys' +import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' +import { EMPTY_JOB_FIELDS, latestJobForTable, latestJobsForTables } from '@/lib/table/jobs/service' +import { nKeysBetween } from '@/lib/table/order-key' +import type { DbTransaction } from '@/lib/table/planner' +import { setTableTxTimeouts } from '@/lib/table/tx' import type { - AddWorkflowGroupData, - BatchInsertData, - BatchUpdateByIdData, - BulkDeleteByIdsData, - BulkDeleteByIdsResult, - BulkDeleteData, - BulkOperationResult, - BulkUpdateData, - ColumnDefinition, CreateTableData, - DeleteColumnData, - DeleteWorkflowGroupData, - Filter, - InsertRowData, - QueryOptions, - QueryResult, - RenameColumnData, - ReplaceRowsData, - ReplaceRowsResult, - RowData, - RowExecutionMetadata, - RowExecutions, - Sort, TableDefinition, - TableDeleteJobPayload, - TableExportJobPayload, - TableJobType, TableMetadata, - TableRow, TableSchema, - UpdateColumnConstraintsData, - UpdateColumnTypeData, - UpdateRowData, - UpdateWorkflowGroupData, - UpsertResult, - UpsertRowData, - WorkflowGroup, - WorkflowGroupOutput, -} from './types' -import { - checkBatchUniqueConstraintsDb, - checkUniqueConstraintsDb, - coerceRowToSchema, - coerceRowValues, - getUniqueColumns, - validateRowSize, - validateTableName, - validateTableSchema, -} from './validation' -import { - assertValidSchema, - cancelWorkflowGroupRuns, - runWorkflowColumn, - stripGroupDeps, -} from './workflow-columns' +} from '@/lib/table/types' +import { validateTableName, validateTableSchema } from '@/lib/table/validation' +import { stripGroupDeps } from '@/lib/table/workflow-columns' const logger = createLogger('TableService') @@ -115,27 +41,6 @@ export class TableConflictError extends Error { export type TableScope = 'active' | 'archived' | 'all' -/** - * Sets per-transaction Postgres timeouts via `SET LOCAL`. - * - * `lock_timeout` is the critical one: without it, a waiter inherits the full - * `statement_timeout` clock, so one stuck writer can drain the pool. - * - * Safe under pgBouncer transaction pooling — `SET LOCAL` is transaction-scoped - * and cleared at COMMIT/ROLLBACK before the session returns to the pool. - */ -async function setTableTxTimeouts( - trx: DbTransaction, - opts?: { statementMs?: number; lockMs?: number; idleMs?: number } -) { - const s = opts?.statementMs ?? 10_000 - const l = opts?.lockMs ?? 3_000 - const i = opts?.idleMs ?? 5_000 - await trx.execute(sql.raw(`SET LOCAL statement_timeout = '${s}ms'`)) - await trx.execute(sql.raw(`SET LOCAL lock_timeout = '${l}ms'`)) - await trx.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = '${i}ms'`)) -} - /** * Serializes schema/metadata read-modify-writes for a single table so * concurrent mutators can't clobber each other's `schema` JSONB @@ -151,7 +56,7 @@ async function setTableTxTimeouts( * the read both release at COMMIT/ROLLBACK; the wait is bounded by the * `statement_timeout` set in `setTableTxTimeouts`. */ -async function withLockedTable( +export async function withLockedTable( tableId: string, mutate: (table: TableDefinition, trx: DbTransaction) => Promise, opts?: { includeArchived?: boolean } @@ -169,56 +74,6 @@ async function withLockedTable( }) } -/** - * Starting `position` for an append import — `max(position) + 1`, or 0 when empty. Read once, - * unlocked, before streaming: the import worker is the table's sole writer, so it can assign - * contiguous positions from this offset without per-batch position scans. - */ -export async function nextImportStartPosition(tableId: string): Promise { - const [{ maxPos }] = await db - .select({ - maxPos: sql`coalesce(max(${userTableRows.position}), -1)`.mapWith(Number), - }) - .from(userTableRows) - .where(eq(userTableRows.tableId, tableId)) - return maxPos + 1 -} - -/** - * Append anchor `order_key` for an import — `max(order_key)`, or null when empty. Read once, - * unlocked, before streaming (the import worker is the table's sole writer); each batch threads - * the previous batch's last key forward so no per-batch max scan is needed. - */ -export async function nextImportStartOrderKey(tableId: string): Promise { - return maxOrderKey(db, tableId) -} - -const TIMEOUT_CAP_MS = 10 * 60_000 - -/** - * Scales `statement_timeout` to the expected row-count work. - * - * Bulk operations that rewrite JSONB or cascade row triggers (e.g. - * `replaceTableRows`, `deleteColumn`, `renameColumn`) scale roughly linearly - * with row count. A fixed cap would regress large-table users who never saw a - * timeout before `SET LOCAL` was introduced. This helper picks - * `max(baseMs, rowCount * perRowMs)`, capped at 10 minutes so a single - * runaway transaction cannot indefinitely pin a pool connection. - */ -function scaledStatementTimeoutMs( - rowCount: number, - opts: { baseMs: number; perRowMs: number } -): number { - const safeRowCount = Math.max(0, rowCount) - return Math.min(TIMEOUT_CAP_MS, Math.max(opts.baseMs, safeRowCount * opts.perRowMs)) -} - -/** - * Gets a table by ID with full details. - * - * @param tableId - Table ID to fetch - * @returns Table definition or null if not found - */ /** * Returns `schema` with `columns` sorted by `metadata.columnOrder` (the user- * editable visible order). Columns missing from `columnOrder` are appended at @@ -250,99 +105,12 @@ function applyColumnOrderToSchema( return { ...schema, columns: ordered } } -/** Job fields projected onto a {@link TableDefinition}, derived from its latest `table_jobs` row. */ -interface DerivedJobFields { - jobStatus: TableDefinition['jobStatus'] - jobId: string | null - jobType: TableDefinition['jobType'] - jobError: string | null - jobRowsProcessed: number - /** - * Rows a running delete job still has to remove (its doomed estimate minus - * deletions so far). Internal to count adjustment — callers subtract it from - * the raw `row_count` so list/detail counts match the read path's delete - * mask (a mid-delete refresh must not resurrect the count). Not on the wire. - */ - pendingDeleteRemaining: number -} - -const EMPTY_JOB_FIELDS: DerivedJobFields = { - jobStatus: null, - jobId: null, - jobType: null, - jobError: null, - jobRowsProcessed: 0, - pendingDeleteRemaining: 0, -} - -function mapJobRow( - row: - | { - id: string - type: string - status: string - rowsProcessed: number - error: string | null - payload: unknown - } - | undefined -): DerivedJobFields { - if (!row) return EMPTY_JOB_FIELDS - const doomedCount = - row.type === 'delete' && row.status === 'running' - ? ((row.payload as TableDeleteJobPayload | null)?.doomedCount ?? 0) - : 0 - return { - jobStatus: row.status as TableDefinition['jobStatus'], - jobId: row.id, - jobType: row.type as TableDefinition['jobType'], - jobError: row.error, - jobRowsProcessed: row.rowsProcessed, - pendingDeleteRemaining: Math.max(0, doomedCount - row.rowsProcessed), - } -} - -const JOB_PROJECTION = { - id: tableJobs.id, - type: tableJobs.type, - status: tableJobs.status, - rowsProcessed: tableJobs.rowsProcessed, - error: tableJobs.error, - payload: tableJobs.payload, -} as const - /** - * The latest job for one table (the running one if present, else the most recent terminal). - * Exports are excluded: they're read-only, run concurrently with other jobs, and have their own - * client surface — surfacing one here would clobber the import/delete/backfill status the tray - * and SSE consumer derive from these fields. + * Gets a table by ID with full details. + * + * @param tableId - Table ID to fetch + * @returns Table definition or null if not found */ -async function latestJobForTable( - tableId: string, - executor: DbOrTx = db -): Promise { - const [row] = await executor - .select(JOB_PROJECTION) - .from(tableJobs) - .where(and(eq(tableJobs.tableId, tableId), ne(tableJobs.type, 'export'))) - .orderBy(desc(tableJobs.startedAt)) - .limit(1) - return mapJobRow(row) -} - -/** Latest non-export job per table for a batch of ids, via `DISTINCT ON (table_id)`. */ -async function latestJobsForTables(tableIds: string[]): Promise> { - const map = new Map() - if (tableIds.length === 0) return map - const rows = await db - .selectDistinctOn([tableJobs.tableId], { tableId: tableJobs.tableId, ...JOB_PROJECTION }) - .from(tableJobs) - .where(and(inArray(tableJobs.tableId, tableIds), ne(tableJobs.type, 'export'))) - .orderBy(tableJobs.tableId, desc(tableJobs.startedAt)) - for (const row of rows) map.set(row.tableId, mapJobRow(row)) - return map -} - export async function getTableById( tableId: string, options?: { includeArchived?: boolean; tx?: DbOrTx } @@ -400,19 +168,6 @@ export async function getTableById( * @param workspaceId - Workspace ID to list tables for * @returns Array of table definitions */ -async function countTables(workspaceId: string): Promise { - const [result] = await db - .select({ count: count() }) - .from(userTableDefinitions) - .where( - and( - eq(userTableDefinitions.workspaceId, workspaceId), - isNull(userTableDefinitions.archivedAt) - ) - ) - return result.count -} - export async function listTables( workspaceId: string, options?: { scope?: TableScope } @@ -625,118 +380,6 @@ export async function createTable( } } -/** - * Adds a column to an existing table's schema. - * - * @param tableId - Table ID to update - * @param column - Column definition to add - * @param requestId - Request ID for logging - * @returns Updated table definition - * @throws Error if table not found or column name already exists - */ -export async function addTableColumn( - tableId: string, - column: { - id?: string - name: string - type: string - required?: boolean - unique?: boolean - position?: number - }, - requestId: string -): Promise { - return withLockedTable(tableId, async (table, trx) => { - if (!NAME_PATTERN.test(column.name)) { - throw new Error( - `Invalid column name "${column.name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.` - ) - } - - if (column.name.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { - throw new Error( - `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` - ) - } - - if (!COLUMN_TYPES.includes(column.type as (typeof COLUMN_TYPES)[number])) { - throw new Error( - `Invalid column type "${column.type}". Must be one of: ${COLUMN_TYPES.join(', ')}` - ) - } - - const schema = table.schema - if (schema.columns.some((c) => c.name.toLowerCase() === column.name.toLowerCase())) { - throw new Error(`Column "${column.name}" already exists`) - } - - if (schema.columns.length >= TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { - throw new Error( - `Table has reached maximum column limit (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE})` - ) - } - - const newColumn: TableSchema['columns'][number] = { - // Honor a caller-provided id (undo of a delete reuses the original id); - // otherwise mint a fresh one. - id: column.id ?? generateColumnId(), - name: column.name, - type: column.type as TableSchema['columns'][number]['type'], - required: column.required ?? false, - unique: column.unique ?? false, - } - const newColumnId = getColumnId(newColumn) - - const columns = [...schema.columns] - if (column.position !== undefined && column.position >= 0 && column.position < columns.length) { - columns.splice(column.position, 0, newColumn) - } else { - columns.push(newColumn) - } - - const updatedSchema: TableSchema = { ...schema, columns } - - // Keep `metadata.columnOrder` (a list of column ids) in sync: splicing the - // new column's id at the same index we used in `columns` keeps display - // ordering aligned with the user's intent for `position`-based inserts. - const existingOrder = table.metadata?.columnOrder - let updatedMetadata = table.metadata - if (existingOrder && existingOrder.length > 0 && !existingOrder.includes(newColumnId)) { - let insertIdx = existingOrder.length - if (column.position !== undefined && column.position >= 0) { - // Anchor on the column previously at `position` — that column shifted - // right by one in `columns`, so the new id slots in at its old spot. - const anchor = schema.columns[column.position] - if (anchor) { - const anchorIdx = existingOrder.indexOf(getColumnId(anchor)) - if (anchorIdx !== -1) insertIdx = anchorIdx - } - } - const nextOrder = [...existingOrder] - nextOrder.splice(insertIdx, 0, newColumnId) - updatedMetadata = { ...table.metadata, columnOrder: nextOrder } - } - - assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) - - const now = new Date() - - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, tableId)) - - logger.info(`[${requestId}] Added column "${column.name}" to table ${tableId}`) - - return { - ...table, - schema: updatedSchema, - metadata: updatedMetadata, - updatedAt: now, - } - }) -} - /** * Adds multiple columns to an existing table inside a caller-provided * transaction. This is atomic with respect to the surrounding `trx`: either @@ -942,80 +585,6 @@ export async function deleteTable(tableId: string, requestId: string): Promise - requestId: string - tx?: DbOrTx -}): Promise { - const executor = tx ?? db - const tables = await executor - .select({ - id: userTableDefinitions.id, - schema: userTableDefinitions.schema, - }) - .from(userTableDefinitions) - .where( - and( - eq(userTableDefinitions.workspaceId, workspaceId), - isNull(userTableDefinitions.archivedAt) - ) - ) - - for (const t of tables) { - const schema = t.schema as TableSchema - const groups = schema.workflowGroups ?? [] - if (groups.length === 0) continue - - let mutated = false - const nextGroups = groups.map((group) => { - if (group.workflowId !== workflowId) return group - const filtered = group.outputs.filter((o) => validBlockIds.has(o.blockId)) - if (filtered.length === group.outputs.length) return group - if (filtered.length === 0) { - logger.warn( - `[${requestId}] All outputs for workflow group "${group.name ?? group.id}" in table ${t.id} reference deleted blocks; leaving group intact for user reconfiguration.` - ) - return group - } - mutated = true - return { ...group, outputs: filtered } - }) - - if (!mutated) continue - - await executor - .update(userTableDefinitions) - .set({ - schema: { ...schema, workflowGroups: nextGroups }, - updatedAt: new Date(), - }) - .where(eq(userTableDefinitions.id, t.id)) - - logger.info(`[${requestId}] Pruned stale workflow=${workflowId} block refs from table ${t.id}`) - } -} - /** * Restores an archived table. */ @@ -1085,4240 +654,3 @@ export async function restoreTable(tableId: string, requestId: string): Promise< logger.info(`[${requestId}] Restored table ${tableId} as "${attemptedRestoreName}"`) } - -/** - * Loads `tableRowExecutions` rows for the given row ids and groups them into a - * `Map` suitable for plugging into `TableRow.executions`. - */ -async function loadExecutionsByRow( - trx: DbOrTx, - rowIds: Iterable -): Promise> { - const ids = Array.from(new Set(rowIds)) - const result = new Map() - if (ids.length === 0) return result - const rows = await trx - .select() - .from(tableRowExecutions) - .where(inArray(tableRowExecutions.rowId, ids)) - for (const r of rows) { - const existing = result.get(r.rowId) ?? {} - const meta: RowExecutionMetadata = { - status: r.status as RowExecutionMetadata['status'], - executionId: r.executionId ?? null, - jobId: r.jobId ?? null, - workflowId: r.workflowId, - error: r.error ?? null, - ...(r.runningBlockIds && r.runningBlockIds.length > 0 - ? { runningBlockIds: r.runningBlockIds } - : {}), - ...(r.blockErrors && Object.keys(r.blockErrors as Record).length > 0 - ? { blockErrors: r.blockErrors as Record } - : {}), - ...(r.cancelledAt ? { cancelledAt: r.cancelledAt.toISOString() } : {}), - } - existing[r.groupId] = meta - result.set(r.rowId, existing) - } - return result -} - -/** Convenience: load executions for one row, returning `{}` when missing. */ -async function loadExecutionsForRow(trx: DbOrTx, rowId: string): Promise { - const byRow = await loadExecutionsByRow(trx, [rowId]) - return byRow.get(rowId) ?? {} -} - -/** - * Serializes writers that assign `position` for the same table. The row-count - * trigger (migration 0198) serializes capacity via a row lock on - * `user_table_definitions`, but it fires AFTER INSERT, so two concurrent - * auto-positioned inserts could read the same snapshot and assign the same - * position (the `(table_id, position)` index is non-unique). This advisory lock - * restores per-table serialization. Released at COMMIT/ROLLBACK. - */ -async function acquireRowOrderLock(trx: DbTransaction, tableId: string) { - await trx.execute( - sql`SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_rows_pos:${tableId}`}, 0))` - ) -} - -/** Next append position for a table (max(position) + 1, or 0 if empty). */ -async function nextRowPosition(trx: DbTransaction, tableId: string): Promise { - const [{ maxPos }] = await trx - .select({ - maxPos: sql`coalesce(max(${userTableRows.position}), -1)`.mapWith(Number), - }) - .from(userTableRows) - .where(eq(userTableRows.tableId, tableId)) - return maxPos + 1 -} - -/** Largest `order_key` for a table, or `null` when empty — the append anchor for new keys. */ -async function maxOrderKey(executor: DbOrTx, tableId: string): Promise { - const [{ maxKey }] = await executor - .select({ maxKey: sql`max(${userTableRows.orderKey})` }) - .from(userTableRows) - .where(eq(userTableRows.tableId, tableId)) - return maxKey ?? null -} - -/** Shifts every row at or after `position` up by one (`position + 1`). */ -async function shiftRowsUpFrom(trx: DbTransaction, tableId: string, position: number) { - await trx - .update(userTableRows) - .set({ position: sql`position + 1` }) - .where(and(eq(userTableRows.tableId, tableId), gte(userTableRows.position, position))) -} - -/** Shifts every row after `position` down by one (`position - 1`). */ -async function shiftRowsDownAfter(trx: DbTransaction, tableId: string, position: number) { - await trx - .update(userTableRows) - .set({ position: sql`position - 1` }) - .where(and(eq(userTableRows.tableId, tableId), gt(userTableRows.position, position))) -} - -/** - * Reserves the `position` for a single inserted row and returns where to INSERT. - * Acquires the row-order lock, then opens a slot at `requestedPosition` (shifting - * the occupant + tail up) or computes the append position. Caller runs inside a - * transaction. - */ -async function reserveInsertPosition( - trx: DbTransaction, - tableId: string, - requestedPosition?: number -): Promise { - await acquireRowOrderLock(trx, tableId) - if (requestedPosition === undefined) { - return nextRowPosition(trx, tableId) - } - const [existing] = await trx - .select({ id: userTableRows.id }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, requestedPosition))) - .limit(1) - if (existing) { - await shiftRowsUpFrom(trx, tableId, requestedPosition) - } - return requestedPosition -} - -/** - * Reserves positions for a batch of `count` rows. Opens each requested slot - * (ascending, preserving prior gaps) and returns the requested positions in - * original order; otherwise returns a contiguous append range. - */ -async function reserveBatchPositions( - trx: DbTransaction, - tableId: string, - count: number, - requestedPositions?: number[] -): Promise { - await acquireRowOrderLock(trx, tableId) - if (requestedPositions && requestedPositions.length > 0) { - for (const pos of [...requestedPositions].sort((a, b) => a - b)) { - await shiftRowsUpFrom(trx, tableId, pos) - } - return requestedPositions - } - const start = await nextRowPosition(trx, tableId) - return Array.from({ length: count }, (_, i) => start + i) -} - -/** - * Recompacts row positions to be contiguous after a bulk delete. With - * `minDeletedPos`, only rows at/after it are re-numbered; single-row deletes use - * the cheaper {@link shiftRowsDownAfter}. - */ -async function compactPositions(trx: DbTransaction, tableId: string, minDeletedPos?: number) { - if (minDeletedPos === undefined) { - await trx.execute(sql` - UPDATE user_table_rows t - SET position = r.new_pos - FROM ( - SELECT id, ROW_NUMBER() OVER (ORDER BY position) - 1 AS new_pos - FROM user_table_rows - WHERE table_id = ${tableId} - ) r - WHERE t.id = r.id AND t.table_id = ${tableId} AND t.position != r.new_pos - `) - return - } - await trx.execute(sql` - UPDATE user_table_rows t - SET position = r.new_pos - FROM ( - SELECT id, ${minDeletedPos}::int + ROW_NUMBER() OVER (ORDER BY position) - 1 AS new_pos - FROM user_table_rows - WHERE table_id = ${tableId} AND position >= ${minDeletedPos} - ) r - WHERE t.id = r.id AND t.table_id = ${tableId} AND t.position != r.new_pos - `) -} - -/** A row value ready to INSERT into `user_table_rows`, with its assigned order. */ -export interface OrderedRowValue { - id: string - tableId: string - workspaceId: string - data: RowData - position: number - orderKey: string - createdAt: Date - updatedAt: Date - createdBy?: string -} - -/** - * Builds INSERT values for a contiguous run of rows, assigning sequential - * positions `startPosition + i` and the supplied `orderKeys[i]`. Centralizes - * row assignment for callers that write a fresh ordered run (e.g. the copilot - * tool's replace-all write). `orderKeys` must be index-aligned with `rows` — - * mint them once for the whole run with {@link nKeysBetween}. - */ -export function buildOrderedRowValues(opts: { - tableId: string - workspaceId: string - rows: RowData[] - startPosition: number - orderKeys: string[] - now: Date - createdBy?: string - makeId: () => string -}): OrderedRowValue[] { - const { tableId, workspaceId, rows, startPosition, orderKeys, now, createdBy, makeId } = opts - return rows.map((data, i) => ({ - id: makeId(), - tableId, - workspaceId, - data, - position: startPosition + i, - orderKey: orderKeys[i], - createdAt: now, - updatedAt: now, - ...(createdBy ? { createdBy } : {}), - })) -} - -/** - * Computes the fractional `order_key` for a row inserted at the integer - * `requestedPosition` (or appended when omitted). Used by position-based callers - * (mothership tool, v1 API, undo position-fallback, transient old clients). - * - * The neighbor at slot `s` is resolved differently per flag state: - * - **off**: `WHERE position = s` (positions are contiguous, so the row at - * position `s` is the `s`-th row — an indexed O(1) lookup). - * - **on**: the `s`-th row in `order_key, id` order (`OFFSET s`) — positions are - * gappy and non-authoritative, so `position = s` would miss; the visual - * ordinal is the key's ordinal. O(s), acceptable for these low-volume callers. - * - * Caller holds the row-order lock. - */ -async function resolveInsertOrderKey( - trx: DbTransaction, - tableId: string, - requestedPosition?: number -): Promise { - const orderKeyAtSlot = async (slot: number): Promise => { - if (slot < 0) return null - if (isTablesFractionalOrderingEnabled) { - const [r] = await trx - .select({ orderKey: userTableRows.orderKey }) - .from(userTableRows) - .where(eq(userTableRows.tableId, tableId)) - .orderBy(asc(userTableRows.orderKey), asc(userTableRows.id)) - .limit(1) - .offset(slot) - return r?.orderKey ?? null - } - const [r] = await trx - .select({ orderKey: userTableRows.orderKey }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, slot))) - .limit(1) - return r?.orderKey ?? null - } - if (requestedPosition === undefined) { - return keyBetween(await maxOrderKey(trx, tableId), null) - } - const lo = await orderKeyAtSlot(requestedPosition - 1) - const hi = await orderKeyAtSlot(requestedPosition) - return keyBetween(lo, hi) -} - -/** - * Resolves the `order_key` for an insert expressed by an anchor row id — - * `afterRowId` (place directly after) or `beforeRowId` (directly before). Finds - * the anchor and its adjacent key via the `(table_id, order_key, id)` index - * (O(1)) and mints a key between them. Also returns a legacy integer `position` - * (anchor's position ±) so the flag-off shift path still works. Caller holds the - * row-order lock. - */ -async function resolveInsertByNeighbor( - trx: DbTransaction, - tableId: string, - afterRowId?: string, - beforeRowId?: string -): Promise<{ orderKey: string; position: number }> { - const anchorId = afterRowId ?? beforeRowId! - const [anchor] = await trx - .select({ orderKey: userTableRows.orderKey, position: userTableRows.position }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.id, anchorId))) - .limit(1) - // The client targets a specific neighbor; a missing one (concurrent delete / - // stale view) is an error, not a silent insert at the front. - if (!anchor) throw new Error(`Row not found: ${anchorId}`) - const anchorKey = anchor.orderKey ?? null - // A null key on the anchor means the table isn't backfilled. With the flag on - // (key is authoritative) the adjacent-key lookup below can't work — fail - // loudly rather than mint a wrong key. Flag off keeps `position` authoritative, - // so a best-effort key here is fine (the backfill re-keys before the flip). - if (anchorKey === null && isTablesFractionalOrderingEnabled) { - throw new Error(`Row ${anchorId} has no order_key yet (table not backfilled)`) - } - - if (afterRowId) { - // hi = the smallest key strictly GREATER than the anchor key. Comparing keys - // (not the `(order_key, id)` row tuple) skips past any sibling that shares the - // anchor's key, so `keyBetween` always gets strictly-ordered bounds and can't - // throw on a stray duplicate. Identical to the row tuple when keys are distinct. - // A null anchorKey (flag off, un-backfilled) has no key to compare — leave the - // upper bound open, matching the prior best-effort behavior. - let nextKey: string | null = null - if (anchorKey !== null) { - const [next] = await trx - .select({ orderKey: userTableRows.orderKey }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), gt(userTableRows.orderKey, anchorKey))) - .orderBy(asc(userTableRows.orderKey)) - .limit(1) - nextKey = next?.orderKey ?? null - } - return { - orderKey: keyBetween(anchorKey, nextKey), - position: anchor.position + 1, - } - } - - // beforeRowId: lo = the largest key strictly LESS than the anchor key (distinct, - // same rationale as the afterRowId branch above). - let prevKey: string | null = null - if (anchorKey !== null) { - const [prev] = await trx - .select({ orderKey: userTableRows.orderKey }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), lt(userTableRows.orderKey, anchorKey))) - .orderBy(desc(userTableRows.orderKey)) - .limit(1) - prevKey = prev?.orderKey ?? null - } - return { - orderKey: keyBetween(prevKey, anchorKey), - position: anchor.position, - } -} - -/** - * Computes fractional `order_key`s for a batch insert. With no `positions`, - * appends a contiguous run after the current max key. With explicit `positions` - * (undo restore), keys each row between its pre-shift position neighbors — - * correct because requested positions are distinct. Caller holds the lock. - * - * The explicit-`positions` path is meaningful only when `position` is - * authoritative (flag off): with the flag on, a saved `position` is a gappy - * column value, not a visual rank, so feeding it to {@link resolveInsertOrderKey} - * (which reads `position` as an `OFFSET` rank under the flag) would mint keys at - * the wrong ranks. Callers needing exact placement under the flag pass - * `orderKeys` (handled before this function); here we just append a run. - */ -async function resolveBatchInsertOrderKeys( - trx: DbTransaction, - tableId: string, - count: number, - positions?: number[] -): Promise { - if (!positions || positions.length === 0 || isTablesFractionalOrderingEnabled) { - return nKeysBetween(await maxOrderKey(trx, tableId), null, count) - } - const keys: string[] = [] - for (const pos of positions) { - keys.push(await resolveInsertOrderKey(trx, tableId, pos)) - } - return keys -} - -/** - * Inserts a single row in its own transaction. Always assigns a fractional - * `order_key`. When the fractional-ordering flag is on, `order_key` is - * authoritative and `position` is a best-effort append (no O(N) shift); when - * off, `position` is reserved as before (shifting to open the slot). Validation - * and side-effect dispatch stay with the caller; capacity is enforced by the - * `increment_user_table_row_count` trigger. - */ -async function insertOrderedRow(params: { - tableId: string - workspaceId: string - data: RowData - rowId: string - position?: number - afterRowId?: string - beforeRowId?: string - createdBy?: string - now: Date -}): Promise<{ - id: string - data: RowData - position: number - orderKey: string | null - createdAt: Date - updatedAt: Date -}> { - const { tableId, workspaceId, data, rowId, position, afterRowId, beforeRowId, createdBy, now } = - params - const [row] = await db.transaction(async (trx) => { - await setTableTxTimeouts(trx) - await acquireRowOrderLock(trx, tableId) - - // Resolve the order key (and a legacy slot position for the flag-off shift - // path) from neighbor ids when given, else from the requested position. - let orderKey: string - let slotPosition = position - if (afterRowId || beforeRowId) { - const resolved = await resolveInsertByNeighbor(trx, tableId, afterRowId, beforeRowId) - orderKey = resolved.orderKey - slotPosition = resolved.position - } else { - orderKey = await resolveInsertOrderKey(trx, tableId, position) - } - - let targetPosition: number - if (isTablesFractionalOrderingEnabled) { - // order_key is authoritative — keep a best-effort, no-shift position. - targetPosition = await nextRowPosition(trx, tableId) - } else if (slotPosition !== undefined) { - const [existing] = await trx - .select({ id: userTableRows.id }) - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), eq(userTableRows.position, slotPosition))) - .limit(1) - if (existing) await shiftRowsUpFrom(trx, tableId, slotPosition) - targetPosition = slotPosition - } else { - targetPosition = await nextRowPosition(trx, tableId) - } - - return trx - .insert(userTableRows) - .values({ - id: rowId, - tableId, - workspaceId, - data, - position: targetPosition, - orderKey, - createdAt: now, - updatedAt: now, - ...(createdBy ? { createdBy } : {}), - }) - .returning() - }) - return { - id: row.id, - data: row.data as RowData, - position: row.position, - orderKey: row.orderKey, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - } -} - -/** - * Deletes a single row by id in its own transaction, then closes the positional - * gap. Returns `false` when no row matched. - */ -async function deleteOrderedRow(params: { - tableId: string - rowId: string - workspaceId: string -}): Promise { - const { tableId, rowId, workspaceId } = params - return db.transaction(async (trx) => { - await setTableTxTimeouts(trx) - const [deleted] = await trx - .delete(userTableRows) - .where( - and( - eq(userTableRows.id, rowId), - eq(userTableRows.tableId, tableId), - eq(userTableRows.workspaceId, workspaceId) - ) - ) - .returning({ position: userTableRows.position }) - if (!deleted) return false - // Fractional ordering: deleting a row never changes another row's order_key, - // so the O(N) position reshift is skipped entirely. - if (!isTablesFractionalOrderingEnabled) { - await shiftRowsDownAfter(trx, tableId, deleted.position) - } - return true - }) -} - -/** - * Deletes the given row ids in batches within one transaction, then recompacts - * positions from the earliest deleted slot. Returns the deleted rows (id + prior - * position). The caller resolves which ids to delete (used by both delete-by-ids - * and delete-by-filter). - */ -async function deleteOrderedRowsByIds(params: { - tableId: string - workspaceId: string - rowIds: string[] -}): Promise<{ id: string; position: number }[]> { - const { tableId, workspaceId, rowIds } = params - if (rowIds.length === 0) return [] - return db.transaction(async (trx) => { - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - const deleted: { id: string; position: number }[] = [] - for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) { - const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE) - const rows = await trx - .delete(userTableRows) - .where( - and( - eq(userTableRows.tableId, tableId), - eq(userTableRows.workspaceId, workspaceId), - inArray(userTableRows.id, batch) - ) - ) - .returning({ id: userTableRows.id, position: userTableRows.position }) - deleted.push(...rows) - } - // Fractional ordering: deletes leave order_key untouched, so no recompaction. - if (!isTablesFractionalOrderingEnabled && deleted.length > 0) { - const minDeletedPos = deleted.reduce( - (min, r) => (r.position < min ? r.position : min), - deleted[0].position - ) - await compactPositions(trx, tableId, minDeletedPos) - } - return deleted - }) -} - -/** - * Selects one page of row ids to delete for the async delete-job worker: base scope plus a - * `created_at <= cutoff` floor (so rows inserted after the job started are never selected) and - * the caller's optional filter clause. Keyset paginated on `id` via `afterId` so excluded rows - * (which are skipped, not deleted) still advance the cursor — no OFFSET, no risk of looping on a - * fully-excluded page. - */ -export async function selectRowIdPage(params: { - tableId: string - workspaceId: string - cutoff: Date - filterClause?: SQL - afterId?: string - limit: number -}): Promise { - const { tableId, workspaceId, cutoff, filterClause, afterId, limit } = params - const selectPage = (executor: DbExecutor) => - executor - .select({ id: userTableRows.id }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, tableId), - eq(userTableRows.workspaceId, workspaceId), - lte(userTableRows.createdAt, cutoff), - afterId ? gt(userTableRows.id, afterId) : undefined, - filterClause - ) - ) - .orderBy(asc(userTableRows.id)) - .limit(limit) - // A jsonb filter is unestimatable, so the planner would seq-scan the whole shared relation - // per page (12.6s measured) — keep it on the tenant's (table_id, id) index. - const rows = filterClause - ? await withSeqscanOff(async (trx) => selectPage(trx)) - : await selectPage(db) - return rows.map((r) => r.id) -} - -/** - * Deletes one page of rows for the async delete-job worker, committing each `DELETE_BATCH_SIZE` - * chunk in its own short transaction. One statement per transaction bounds how long the - * statement-level row_count trigger's lock on the definition row is held (a page-wide transaction - * held it for the entire page, starving concurrent inserts and overrunning `statement_timeout`), - * and a mid-page failure loses at most one uncommitted batch — the keyset walker (or a task - * retry) re-walks whatever remains. Skips legacy position compaction: under fractional ordering - * it's unnecessary, and in the legacy path `position` gaps are harmless — rows still order by - * position. Returns the count deleted. - */ -export async function deletePageByIds( - tableId: string, - workspaceId: string, - rowIds: string[] -): Promise { - let deleted = 0 - for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) { - const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE) - const rows = await db.transaction(async (trx) => { - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - return trx - .delete(userTableRows) - .where( - and( - eq(userTableRows.tableId, tableId), - eq(userTableRows.workspaceId, workspaceId), - inArray(userTableRows.id, batch) - ) - ) - .returning({ id: userTableRows.id }) - }) - deleted += rows.length - } - return deleted -} - -/** - * Inserts a single row into a table. - * - * @param data - Row insertion data - * @param table - Table definition (to avoid re-fetching) - * @param requestId - Request ID for logging - * @returns Inserted row - * @throws Error if validation fails or capacity exceeded - */ -export async function insertRow( - data: InsertRowData, - table: TableDefinition, - requestId: string -): Promise { - // Validate row size - const sizeValidation = validateRowSize(data.data) - if (!sizeValidation.valid) { - throw new Error(sizeValidation.errors.join(', ')) - } - - // Validate against schema - const schemaValidation = coerceRowToSchema(data.data, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) - } - - // Check unique constraints using optimized database query - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0) { - const uniqueValidation = await checkUniqueConstraintsDb(data.tableId, data.data, table.schema) - if (!uniqueValidation.valid) { - throw new Error(uniqueValidation.errors.join(', ')) - } - } - - const rowId = `row_${generateId().replace(/-/g, '')}` - const now = new Date() - - // Capacity enforcement lives in the `increment_user_table_row_count` trigger - // (migration 0198): a single conditional UPDATE on user_table_definitions - // increments row_count iff row_count < max_rows, taking the row lock - // atomically. No app-level FOR UPDATE / COUNT needed. - const row = await insertOrderedRow({ - tableId: data.tableId, - workspaceId: data.workspaceId, - data: data.data, - rowId, - position: data.position, - afterRowId: data.afterRowId, - beforeRowId: data.beforeRowId, - createdBy: data.userId, - now, - }) - - logger.info(`[${requestId}] Inserted row ${rowId} into table ${data.tableId}`) - - const insertedRow: TableRow = { - id: row.id, - data: row.data as RowData, - executions: {}, - position: row.position, - orderKey: row.orderKey ?? undefined, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - } - - void fireTableTrigger( - data.tableId, - table.name, - 'insert', - [insertedRow], - null, - table.schema, - requestId - ) - void runWorkflowColumn({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds: [insertedRow.id], - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: data.userId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (insertRow) failed:`, err)) - - return insertedRow -} - -/** - * Inserts multiple rows into a table. - * - * @param data - Batch insertion data - * @param table - Table definition - * @param requestId - Request ID for logging - * @returns Array of inserted rows - * @throws Error if validation fails or capacity exceeded - */ -export async function batchInsertRows( - data: BatchInsertData, - table: TableDefinition, - requestId: string -): Promise { - const result = await db.transaction((trx) => batchInsertRowsWithTx(trx, data, table, requestId)) - dispatchAfterBatchInsert(table, result, requestId, data.userId) - return result -} - -/** - * Transaction-bound variant of `batchInsertRows`. Validates rows and unique - * constraints, then performs INSERTs inside the provided transaction. Caller - * is responsible for opening the transaction. Use when row inserts must be - * atomic with other writes (e.g., schema mutations) on the same tx. - * - * Capacity enforcement lives in the `increment_user_table_row_count` trigger - * (migration 0198) — fires per row and raises `Maximum row limit (%) reached ...` - * if the cap is hit mid-batch. - */ -export async function batchInsertRowsWithTx( - trx: DbTransaction, - data: BatchInsertData, - table: TableDefinition, - requestId: string -): Promise { - for (let i = 0; i < data.rows.length; i++) { - const row = data.rows[i] - - const sizeValidation = validateRowSize(row) - if (!sizeValidation.valid) { - throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) - } - - const schemaValidation = coerceRowToSchema(row, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) - } - } - - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0) { - const uniqueResult = await checkBatchUniqueConstraintsDb( - data.tableId, - data.rows, - table.schema, - trx - ) - if (!uniqueResult.valid) { - const errorMessages = uniqueResult.errors - .map((e) => `Row ${e.row + 1}: ${e.errors.join(', ')}`) - .join('; ') - throw new Error(errorMessages) - } - } - - const now = new Date() - - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - - const buildRow = (rowData: RowData, position: number, orderKey: string) => ({ - id: `row_${generateId().replace(/-/g, '')}`, - tableId: data.tableId, - workspaceId: data.workspaceId, - data: rowData, - position, - orderKey, - createdAt: now, - updatedAt: now, - ...(data.userId ? { createdBy: data.userId } : {}), - }) - - await acquireRowOrderLock(trx, data.tableId) - // Undo restore passes exact saved keys; otherwise derive from positions/append. - const orderKeys = - data.orderKeys && data.orderKeys.length > 0 - ? data.orderKeys - : await resolveBatchInsertOrderKeys(trx, data.tableId, data.rows.length, data.positions) - let positions: number[] - if (isTablesFractionalOrderingEnabled) { - // order_key authoritative — best-effort append positions, no shift. - const start = await nextRowPosition(trx, data.tableId) - positions = Array.from({ length: data.rows.length }, (_, i) => start + i) - } else { - positions = await reserveBatchPositions(trx, data.tableId, data.rows.length, data.positions) - } - const rowsToInsert = data.rows.map((rowData, i) => buildRow(rowData, positions[i], orderKeys[i])) - const insertedRows = await trx.insert(userTableRows).values(rowsToInsert).returning() - - logger.info(`[${requestId}] Batch inserted ${data.rows.length} rows into table ${data.tableId}`) - - const result: TableRow[] = insertedRows.map((r) => ({ - id: r.id, - data: r.data as RowData, - executions: {}, - position: r.position, - orderKey: r.orderKey ?? undefined, - createdAt: r.createdAt, - updatedAt: r.updatedAt, - })) - - return result -} - -/** - * Side-effect dispatch for an insert batch. Caller fires this AFTER the - * surrounding transaction commits — `fireTableTrigger` and `runWorkflowColumn` - * both read through the global db connection, so firing inside the tx can see - * no rows and no-op. - */ -export function dispatchAfterBatchInsert( - table: TableDefinition, - result: TableRow[], - requestId: string, - actorUserId?: string | null -): void { - void fireTableTrigger(table.id, table.name, 'insert', result, null, table.schema, requestId) - // Scope to the newly-inserted row ids so the dispatcher doesn't walk every - // row in the table. After the sidecar migration, all existing rows have - // zero entries → `mode:'new'`'s `NOT EXISTS` filter would otherwise include - // them, dispatching workflows on every row in a populated table. - void runWorkflowColumn({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds: result.map((r) => r.id), - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: actorUserId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchInsertRows) failed:`, err)) -} - -/** One batch of rows for a background import (see {@link bulkInsertImportBatch}). */ -export interface BulkImportBatch { - tableId: string - workspaceId: string - userId?: string - rows: RowData[] - /** Position of the first row in this batch; rows get contiguous positions from here. */ - startPosition: number - /** Previous batch's last `order_key` (the append anchor); null for the first batch / empty table. */ - afterOrderKey?: string | null -} - -/** - * Inserts one batch of rows for an async import in a single committed statement. - * - * Differs from {@link batchInsertRowsWithTx} for the bulk-load case: caller-supplied - * contiguous positions (no `acquireTablePositionLock` / `nextAutoPosition` scan — an - * import owns its hidden table as the sole writer), no `RETURNING`, and **no - * `fireTableTrigger` / `runWorkflowColumn`** (a 1M-row import must not dispatch a - * workflow run per row). `row_count` is maintained set-based by the statement-level - * trigger. There is no surrounding transaction and no rollback: each batch commits on - * its own, so committed batches persist even if a later batch fails. - * - * Throws on row-size/schema/unique violations or if the statement-level trigger rejects - * the batch for crossing `max_rows`; the caller marks the import failed. - */ -export async function bulkInsertImportBatch( - data: BulkImportBatch, - table: TableDefinition, - requestId: string -): Promise<{ inserted: number; lastOrderKey: string | null }> { - for (let i = 0; i < data.rows.length; i++) { - const sizeValidation = validateRowSize(data.rows[i]) - if (!sizeValidation.valid) { - throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) - } - const schemaValidation = coerceRowToSchema(data.rows[i], table.schema) - if (!schemaValidation.valid) { - throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) - } - } - - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0) { - const uniqueResult = await checkBatchUniqueConstraintsDb( - data.tableId, - data.rows, - table.schema, - db - ) - if (!uniqueResult.valid) { - throw new Error( - uniqueResult.errors.map((e) => `Row ${e.row + 1}: ${e.errors.join(', ')}`).join('; ') - ) - } - } - - const now = new Date() - // Import worker is the table's sole writer; append keys after the anchor the caller threads - // from the previous batch's last key — no per-batch max(order_key) scan over a growing table. - const orderKeys = nKeysBetween(data.afterOrderKey ?? null, null, data.rows.length) - const rowsToInsert = data.rows.map((rowData, i) => ({ - id: `row_${generateId().replace(/-/g, '')}`, - tableId: data.tableId, - workspaceId: data.workspaceId, - data: rowData, - position: data.startPosition + i, - orderKey: orderKeys[i], - createdAt: now, - updatedAt: now, - ...(data.userId ? { createdBy: data.userId } : {}), - })) - - await db.insert(userTableRows).values(rowsToInsert) - logger.info(`[${requestId}] Bulk-imported ${rowsToInsert.length} rows into table ${data.tableId}`) - return { - inserted: rowsToInsert.length, - lastOrderKey: orderKeys[orderKeys.length - 1] ?? data.afterOrderKey ?? null, - } -} - -/** Deletes every row of a table (set-based; the statement-level trigger zeroes `row_count`). */ -export async function deleteAllTableRows(tableId: string): Promise { - await db.delete(userTableRows).where(eq(userTableRows.tableId, tableId)) -} - -/** - * Adds columns to a table during an import (the `createColumns` flow), wrapping the - * tx-bound {@link addTableColumnsWithTx} in its own transaction. Returns the updated table. - */ -export async function addImportColumns( - table: TableDefinition, - additions: { name: string; type: string }[], - requestId: string -): Promise { - return db.transaction((trx) => addTableColumnsWithTx(trx, table, additions, requestId)) -} - -/** Overwrites a table's schema during an import (used when inferring columns from the file). */ -export async function setTableSchemaForImport(tableId: string, schema: TableSchema): Promise { - await db - .update(userTableDefinitions) - .set({ schema, updatedAt: new Date() }) - .where(eq(userTableDefinitions.id, tableId)) -} - -/** - * Atomically claims a table's single background-job slot by inserting a `running` row into - * `table_jobs`. The partial-unique index on `table_id WHERE status = 'running'` is the - * concurrency gate: a second insert while a job runs hits `ON CONFLICT DO NOTHING` and returns no - * row, so import and delete (and two imports) are mutually exclusive for free. Returns whether it - * claimed the slot; the caller returns 409 when it didn't. - */ -export async function markTableJobRunning( - tableId: string, - jobId: string, - type: TableJobType, - /** Type-specific scope persisted to `table_jobs.payload` (e.g. {@link TableDeleteJobPayload}) - * so read paths can mask the job's effect while it runs. */ - payload?: unknown -): Promise { - // workspace_id is immutable; the atomic gate is the INSERT's conflict, not this read. - const [def] = await db - .select({ workspaceId: userTableDefinitions.workspaceId }) - .from(userTableDefinitions) - .where(eq(userTableDefinitions.id, tableId)) - .limit(1) - if (!def) return false - const inserted = await db - .insert(tableJobs) - .values({ - id: jobId, - tableId, - workspaceId: def.workspaceId, - type, - status: 'running', - payload: payload ?? null, - }) - .onConflictDoNothing() - .returning({ id: tableJobs.id }) - return inserted.length > 0 -} - -/** - * Releases a claim taken by {@link markTableJobRunning} for a synchronous job — deletes the - * transient claim row. Scoped to `jobId` + still-running so it only clears its own claim, never a - * newer run. A sync route claims, writes, then releases here in a `finally`. - */ -export async function releaseJobClaim(tableId: string, jobId: string): Promise { - await db - .delete(tableJobs) - .where( - and(eq(tableJobs.id, jobId), eq(tableJobs.tableId, tableId), eq(tableJobs.status, 'running')) - ) -} - -/** - * Records job progress (rows processed so far) and bumps `updated_at` so the stale-job janitor - * (`cleanup-stale-executions`) sees a live heartbeat. - * - * Scoped to `jobId` AND `status = 'running'`: a stale/superseded worker no longer matches (its - * write is a no-op), and once the job is terminal (e.g. canceled) the match fails too — so this - * returning `false` is the worker's signal to stop. Returns whether this worker still owns an - * in-flight job. - */ -export async function updateJobProgress( - tableId: string, - rowsProcessed: number, - jobId: string -): Promise { - const updated = await db - .update(tableJobs) - .set({ rowsProcessed, updatedAt: new Date() }) - .where(ownsActiveJob(tableId, jobId)) - .returning({ id: tableJobs.id }) - return updated.length > 0 -} - -/** - * Reads the persisted progress of an in-flight job this worker still owns (`null` when the job - * was canceled/superseded). A retried run seeds its counter from this so progress stays - * cumulative — earlier attempts' batches are already committed, and restarting from zero would - * clobber `rows_processed` (and every count derived from it) with the retry's smaller number. - */ -export async function getJobProgress(tableId: string, jobId: string): Promise { - const [job] = await db - .select({ rowsProcessed: tableJobs.rowsProcessed }) - .from(tableJobs) - .where(ownsActiveJob(tableId, jobId)) - .limit(1) - return job ? job.rowsProcessed : null -} - -/** - * One keyset page of rows for the export worker, ordered by `(position, id)`. Keyset (not - * OFFSET) keeps each page O(page) — offset paging re-scans every prior row per page, which is - * O(N²) across a large export. `(position, id)` is total (position exists on every row; id breaks - * ties) and served by the `(table_id, position)` index; under fractional ordering a manually - * reordered table may export in near-grid rather than exact grid order — the right trade for a - * bulk dump. The delete-job visibility mask applies, like every user-facing read. - */ -export async function selectExportRowPage( - table: TableDefinition, - after: { position: number; id: string } | null, - limit: number -): Promise> { - const deleteMask = await pendingDeleteMask(table) - const rows = await db - .select({ id: userTableRows.id, data: userTableRows.data, position: userTableRows.position }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, table.id), - eq(userTableRows.workspaceId, table.workspaceId), - deleteMask, - after - ? sql`(${userTableRows.position}, ${userTableRows.id}) > (${after.position}, ${after.id})` - : undefined - ) - ) - .orderBy(asc(userTableRows.position), asc(userTableRows.id)) - .limit(limit) - return rows as Array<{ id: string; data: RowData; position: number }> -} - -/** How long a terminal export stays listable (and re-downloadable from the tray). */ -const EXPORT_JOB_VISIBILITY_MS = 10 * 60 * 1000 - -export interface WorkspaceExportJob { - jobId: string - tableId: string - tableName: string - status: string - rowsProcessed: number - format: 'csv' | 'json' - hasResult: boolean - error: string | null -} - -/** - * Export jobs the tray surfaces for a workspace: everything running, plus terminals from the last - * {@link EXPORT_JOB_VISIBILITY_MS} so a just-finished export stays re-downloadable. Exports live - * outside the table-level job derivation (which excludes them), so this is their read path. - */ -export async function listWorkspaceExportJobs(workspaceId: string): Promise { - const visibilityCutoff = new Date(Date.now() - EXPORT_JOB_VISIBILITY_MS) - const rows = await db - .select({ - jobId: tableJobs.id, - tableId: tableJobs.tableId, - tableName: userTableDefinitions.name, - status: tableJobs.status, - rowsProcessed: tableJobs.rowsProcessed, - payload: tableJobs.payload, - error: tableJobs.error, - }) - .from(tableJobs) - .innerJoin(userTableDefinitions, eq(userTableDefinitions.id, tableJobs.tableId)) - .where( - and( - eq(tableJobs.workspaceId, workspaceId), - eq(tableJobs.type, 'export'), - or(eq(tableJobs.status, 'running'), gt(tableJobs.updatedAt, visibilityCutoff)) - ) - ) - .orderBy(desc(tableJobs.startedAt)) - return rows.map((r) => { - const payload = r.payload as TableExportJobPayload | null - return { - jobId: r.jobId, - tableId: r.tableId, - tableName: r.tableName, - status: r.status, - rowsProcessed: r.rowsProcessed, - format: payload?.format ?? 'csv', - hasResult: Boolean(payload?.resultKey), - error: r.error, - } - }) -} - -/** Reads one job row (type/status/payload) scoped to its table. Null when absent. */ -export async function getTableJob( - tableId: string, - jobId: string -): Promise<{ id: string; type: string; status: string; payload: unknown } | null> { - const [job] = await db - .select({ - id: tableJobs.id, - type: tableJobs.type, - status: tableJobs.status, - payload: tableJobs.payload, - }) - .from(tableJobs) - .where(and(eq(tableJobs.id, jobId), eq(tableJobs.tableId, tableId))) - .limit(1) - return job ?? null -} - -/** - * Stamps an export job's generated-file storage key onto its payload (`{ resultKey }` merge). - * Scoped to the still-running job so a superseded attempt can't clobber a newer run's result. - * The download route reads it; the janitor deletes the file when the terminal job is pruned. - */ -export async function setJobResultKey( - tableId: string, - jobId: string, - resultKey: string -): Promise { - await db - .update(tableJobs) - .set({ - payload: sql`coalesce(${tableJobs.payload}, '{}'::jsonb) || jsonb_build_object('resultKey', ${resultKey}::text)`, - updatedAt: new Date(), - }) - .where(ownsActiveJob(tableId, jobId)) -} - -/** Shared WHERE for terminal transitions: this job run, and still in-flight (write-once). */ -function ownsActiveJob(tableId: string, jobId: string) { - return and( - eq(tableJobs.id, jobId), - eq(tableJobs.tableId, tableId), - eq(tableJobs.status, 'running') - ) -} - -/** - * Marks a job complete. No-op unless it's still this in-flight run. Returns whether it - * transitioned, so the worker only emits the `ready` event when it actually won (and not after a - * cancel / supersede). - */ -export async function markJobReady(tableId: string, jobId: string): Promise { - const now = new Date() - const updated = await db - .update(tableJobs) - .set({ status: 'ready', error: null, completedAt: now, updatedAt: now }) - .where(ownsActiveJob(tableId, jobId)) - .returning({ id: tableJobs.id }) - return updated.length > 0 -} - -/** - * Marks a job failed, leaving any already-committed work in place. No-op unless it's still this - * in-flight run (so a stale worker can't clobber a newer job or a cancel). - */ -export async function markJobFailed(tableId: string, jobId: string, error: string): Promise { - const now = new Date() - await db - .update(tableJobs) - .set({ status: 'failed', error: error.slice(0, 2000), completedAt: now, updatedAt: now }) - .where(ownsActiveJob(tableId, jobId)) -} - -/** - * Marks an in-flight job canceled (user-initiated). No-op unless it's still running. The - * worker's next ownership check then returns `false` and it stops; committed work is left in - * place (no rollback). Returns whether a running job was actually canceled. - */ -export async function markJobCanceled(tableId: string, jobId: string): Promise { - const now = new Date() - const updated = await db - .update(tableJobs) - .set({ status: 'canceled', completedAt: now, updatedAt: now }) - .where(ownsActiveJob(tableId, jobId)) - .returning({ id: tableJobs.id }) - return updated.length > 0 -} - -/** - * Replaces all rows in a table with a new set of rows. Deletes existing rows - * and inserts the provided rows inside a single transaction so the table is - * never observed in an empty intermediate state by other readers. - * - * Validates each row against the schema, enforces unique constraints within the - * new rows (existing rows are deleted, so DB-side checks are unnecessary), and - * enforces `maxRows` before the replace executes. - * - * @param data - Replace data (rows to install) - * @param table - Table definition - * @param requestId - Request ID for logging - * @returns Count of rows deleted and inserted - * @throws Error if validation fails or capacity exceeded - */ -export async function replaceTableRows( - data: ReplaceRowsData, - table: TableDefinition, - requestId: string -): Promise { - return db.transaction((trx) => replaceTableRowsWithTx(trx, data, table, requestId)) -} - -/** - * Transaction-bound variant of `replaceTableRows`. Caller opens the transaction. - * Use when the replace must be atomic with other writes (e.g., schema mutations). - */ -export async function replaceTableRowsWithTx( - trx: DbTransaction, - data: ReplaceRowsData, - table: TableDefinition, - requestId: string -): Promise { - if (data.tableId !== table.id) { - throw new Error(`Table ID mismatch: ${data.tableId} vs ${table.id}`) - } - if (data.workspaceId !== table.workspaceId) { - throw new Error(`Workspace ID mismatch: ${data.workspaceId} does not own table ${data.tableId}`) - } - if (data.rows.length > table.maxRows) { - throw new Error( - `Cannot replace: ${data.rows.length} rows exceeds table row limit (${table.maxRows})` - ) - } - - for (let i = 0; i < data.rows.length; i++) { - const row = data.rows[i] - - const sizeValidation = validateRowSize(row) - if (!sizeValidation.valid) { - throw new Error(`Row ${i + 1}: ${sizeValidation.errors.join(', ')}`) - } - - const schemaValidation = coerceRowToSchema(row, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Row ${i + 1}: ${schemaValidation.errors.join(', ')}`) - } - } - - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0 && data.rows.length > 0) { - const seen = new Map>() - for (const col of uniqueColumns) { - seen.set(col.name, new Map()) - } - for (let i = 0; i < data.rows.length; i++) { - const row = data.rows[i] - for (const col of uniqueColumns) { - const value = row[col.name] - if (value === null || value === undefined) continue - const normalized = typeof value === 'string' ? value.toLowerCase() : JSON.stringify(value) - const map = seen.get(col.name)! - if (map.has(normalized)) { - throw new Error( - `Row ${i + 1}: Column "${col.name}" must be unique. Value "${String(value)}" duplicates row ${map.get(normalized)! + 1} in batch` - ) - } - map.set(normalized, i) - } - } - } - - const now = new Date() - - const totalRowWork = Math.max(0, table.rowCount ?? 0) + data.rows.length - const statementMs = scaledStatementTimeoutMs(totalRowWork, { - baseMs: 120_000, - perRowMs: 3, - }) - - await setTableTxTimeouts(trx, { statementMs }) - - // Serialize concurrent replaces (and concurrent auto-position inserts) on the - // same table. Without this, two concurrent replaces each see their own MVCC - // snapshot for the DELETE; the second's DELETE would not observe rows the - // first inserted, so both transactions commit and the table ends up with - // the union of both row sets instead of only the last caller's rows. - await acquireRowOrderLock(trx, data.tableId) - - const deletedRows = await trx - .delete(userTableRows) - .where(eq(userTableRows.tableId, data.tableId)) - .returning({ id: userTableRows.id }) - - let insertedCount = 0 - if (data.rows.length > 0) { - // All prior rows were just deleted — assign a fresh contiguous key run. - const orderKeys = nKeysBetween(null, null, data.rows.length) - const rowsToInsert = data.rows.map((rowData, i) => ({ - id: `row_${generateId().replace(/-/g, '')}`, - tableId: data.tableId, - workspaceId: data.workspaceId, - data: rowData, - position: i, - orderKey: orderKeys[i], - createdAt: now, - updatedAt: now, - ...(data.userId ? { createdBy: data.userId } : {}), - })) - - const batchSize = TABLE_LIMITS.MAX_BATCH_INSERT_SIZE - for (let i = 0; i < rowsToInsert.length; i += batchSize) { - const chunk = rowsToInsert.slice(i, i + batchSize) - const inserted = await trx.insert(userTableRows).values(chunk).returning({ - id: userTableRows.id, - }) - insertedCount += inserted.length - } - } - - logger.info( - `[${requestId}] Replaced rows in table ${data.tableId}: deleted ${deletedRows.length}, inserted ${insertedCount}` - ) - - return { deletedCount: deletedRows.length, insertedCount } -} - -/** - * Owns the append-import transaction so the API route never holds a `trx`: - * optionally creates the new columns, then inserts every row in CSV-sized - * batches — all atomic. Caller fires {@link dispatchAfterBatchInsert} after this - * resolves (post-commit), mirroring the other batch-insert sites. - */ -export async function importAppendRows( - table: TableDefinition, - additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], - rows: RowData[], - ctx: { workspaceId: string; userId?: string; requestId: string } -): Promise<{ inserted: TableRow[]; table: TableDefinition }> { - return db.transaction(async (trx) => { - let working = table - if (additions.length > 0) { - // Take the row-order lock before creating columns so this path uses the - // same rows_pos → user_table_definitions order as plain inserts. Creating - // columns first would lock the definition row before rows_pos, inverting - // the order and deadlocking concurrent inserts on this table. The lock is - // re-entrant, so the per-batch acquire below is a no-op. - await acquireRowOrderLock(trx, table.id) - working = await addTableColumnsWithTx(trx, table, additions, ctx.requestId) - } - const inserted: TableRow[] = [] - for (let i = 0; i < rows.length; i += CSV_MAX_BATCH_SIZE) { - const batch = rows.slice(i, i + CSV_MAX_BATCH_SIZE) - const batchInserted = await batchInsertRowsWithTx( - trx, - { tableId: working.id, rows: batch, workspaceId: ctx.workspaceId, userId: ctx.userId }, - working, - generateId().slice(0, 8) - ) - inserted.push(...batchInserted) - } - return { inserted, table: working } - }) -} - -/** - * Owns the replace-import transaction: optionally creates the new columns, then - * replaces all rows — atomically. Keeps `trx` out of the API route. - */ -export async function importReplaceRows( - table: TableDefinition, - additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], - data: { rows: RowData[]; workspaceId: string; userId?: string }, - requestId: string -): Promise { - return db.transaction(async (trx) => { - let working = table - if (additions.length > 0) { - await acquireRowOrderLock(trx, table.id) - working = await addTableColumnsWithTx(trx, table, additions, requestId) - } - return replaceTableRowsWithTx( - trx, - { tableId: working.id, rows: data.rows, workspaceId: data.workspaceId, userId: data.userId }, - working, - requestId - ) - }) -} - -/** - * Upserts a row: updates an existing row if a match is found on the conflict target - * column, otherwise inserts a new row. - * - * Uses a single unique column for matching (not OR across all unique columns) to avoid - * ambiguous matches when multiple unique columns exist. Capacity enforcement lives - * in the `increment_user_table_row_count` trigger (migration 0198). On the insert - * path we acquire the per-table advisory lock and re-check for an existing match - * before inserting, so a concurrent upsert racing on the same conflict target - * cannot produce a duplicate row. - * - * @param data - Upsert data including optional conflictTarget - * @param table - Table definition - * @param requestId - Request ID for logging - * @returns The upserted row and whether it was an insert or update - * @throws Error if no unique columns, ambiguous conflict target, or capacity exceeded - */ -export async function upsertRow( - data: UpsertRowData, - table: TableDefinition, - requestId: string -): Promise { - const schema = table.schema - const uniqueColumns = getUniqueColumns(schema) - - if (uniqueColumns.length === 0) { - throw new Error( - 'Upsert requires at least one unique column in the schema. Please add a unique constraint to a column or use insert instead.' - ) - } - - // Determine the single conflict target column, resolving to its stable - // storage id (the row-data key). `conflictTarget` may arrive as an id - // (first-party) or a name (legacy/internal) — match either. - let targetColumnKey: string - if (data.conflictTarget) { - const col = uniqueColumns.find( - (c) => getColumnId(c) === data.conflictTarget || c.name === data.conflictTarget - ) - if (!col) { - throw new Error( - `Column "${data.conflictTarget}" is not a unique column. Available unique columns: ${uniqueColumns.map((c) => c.name).join(', ')}` - ) - } - targetColumnKey = getColumnId(col) - } else if (uniqueColumns.length === 1) { - targetColumnKey = getColumnId(uniqueColumns[0]) - } else { - throw new Error( - `Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify conflictTarget to indicate which column to match on.` - ) - } - - // Validate row data - const sizeValidation = validateRowSize(data.data) - if (!sizeValidation.valid) { - throw new Error(sizeValidation.errors.join(', ')) - } - - const schemaValidation = coerceRowToSchema(data.data, schema) - if (!schemaValidation.valid) { - throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) - } - - // Read the conflict-target value *after* coercion so `matchFilter` branches on - // the persisted type (e.g. a coerced `"123"` → `123` matches existing rows). - const targetValue = data.data[targetColumnKey] - if (targetValue === undefined || targetValue === null) { - // Surface the display name, not the internal id — v1 callers pass a name. - const targetColumnName = - uniqueColumns.find((c) => getColumnId(c) === targetColumnKey)?.name ?? targetColumnKey - throw new Error(`Upsert requires a value for the conflict target column "${targetColumnName}"`) - } - - // `data->` and `data->>` accept the JSON key as a parameterized text value; - // no need for `sql.raw` interpolation. - const matchFilter = - typeof targetValue === 'string' - ? sql`${userTableRows.data}->>${targetColumnKey}::text = ${String(targetValue)}` - : sql`(${userTableRows.data}->${targetColumnKey}::text)::jsonb = ${JSON.stringify(targetValue)}::jsonb` - - // Capacity enforcement for the insert path lives in the `increment_user_table_row_count` - // trigger (migration 0198). The update path doesn't change row_count, so no check needed. - const result = await db.transaction(async (trx) => { - await setTableTxTimeouts(trx) - // The conflict lookups below match on `data->>key` — unestimatable, and an - // insert-path upsert (no existing match) can't exit early, so the planner - // would seq-scan the whole shared relation. See withSeqscanOff. - await trx.execute(sql`SET LOCAL enable_seqscan = off`) - - // Find existing row by single conflict target column - const [existingRow] = await trx - .select() - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, data.tableId), - eq(userTableRows.workspaceId, data.workspaceId), - matchFilter - ) - ) - .limit(1) - - // Check uniqueness on ALL unique columns (not just the conflict target) - const uniqueValidation = await checkUniqueConstraintsDb( - data.tableId, - data.data, - schema, - existingRow?.id, // exclude the matched row on updates - trx - ) - if (!uniqueValidation.valid) { - throw new Error(`Unique constraint violation: ${uniqueValidation.errors.join(', ')}`) - } - - const now = new Date() - - // Resolve which row (if any) we should update. If the initial SELECT missed, - // acquire the lock and re-check — a concurrent upsert may have inserted the - // matching row between our SELECT and the INSERT path; without the re-check - // both transactions would insert and bypass the app-level unique check. - let matchedRowId = existingRow?.id - let previousData = existingRow?.data as RowData | undefined - if (!matchedRowId) { - await acquireRowOrderLock(trx, data.tableId) - const [racedRow] = await trx - .select({ id: userTableRows.id, data: userTableRows.data }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, data.tableId), - eq(userTableRows.workspaceId, data.workspaceId), - matchFilter - ) - ) - .limit(1) - if (racedRow) { - matchedRowId = racedRow.id - previousData = racedRow.data as RowData - } - } - - if (matchedRowId) { - const [updatedRow] = await trx - .update(userTableRows) - .set({ data: data.data, updatedAt: now }) - .where(eq(userTableRows.id, matchedRowId)) - .returning() - - const executions = await loadExecutionsForRow(trx, updatedRow.id) - return { - row: { - id: updatedRow.id, - data: updatedRow.data as RowData, - executions, - position: updatedRow.position, - orderKey: updatedRow.orderKey ?? undefined, - createdAt: updatedRow.createdAt, - updatedAt: updatedRow.updatedAt, - }, - previousData, - operation: 'update' as const, - } - } - - const [insertedRow] = await trx - .insert(userTableRows) - .values({ - id: `row_${generateId().replace(/-/g, '')}`, - tableId: data.tableId, - workspaceId: data.workspaceId, - data: data.data, - position: await reserveInsertPosition(trx, data.tableId), - orderKey: await resolveInsertOrderKey(trx, data.tableId), - createdAt: now, - updatedAt: now, - ...(data.userId ? { createdBy: data.userId } : {}), - }) - .returning() - - return { - row: { - id: insertedRow.id, - data: insertedRow.data as RowData, - executions: {}, - position: insertedRow.position, - orderKey: insertedRow.orderKey ?? undefined, - createdAt: insertedRow.createdAt, - updatedAt: insertedRow.updatedAt, - }, - operation: 'insert' as const, - } - }) - - logger.info( - `[${requestId}] Upserted (${result.operation}) row ${result.row.id} in table ${data.tableId}` - ) - - if (result.operation === 'insert') { - void fireTableTrigger( - data.tableId, - table.name, - 'insert', - [result.row], - null, - table.schema, - requestId - ) - } else if (result.operation === 'update' && result.previousData) { - const oldRows = new Map([[result.row.id, result.previousData]]) - void fireTableTrigger( - data.tableId, - table.name, - 'update', - [result.row], - oldRows, - table.schema, - requestId - ) - } - void runWorkflowColumn({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds: [result.row.id], - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: data.userId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (upsertRow) failed:`, err)) - - return result -} - -/** - * Canonical ORDER BY for a table's rows, shared by `queryRows` (the paginated - * list) and `findRowMatches` so a match's ordinal lines up with its index in - * the list. Order: explicit data sort (if any) → fractional `order_key` or - * legacy `position` → `id`. The `id` tiebreak is always appended so equal - * positions order deterministically — without it two separate query executions - * (a find vs a list page) could shuffle ties and misalign ordinals. - */ -function buildRowOrderBySql( - sort: Sort | undefined, - tableName: string, - columns: ColumnDefinition[] -): SQL { - const primary = isTablesFractionalOrderingEnabled - ? `${tableName}.order_key` - : `${tableName}.position` - const id = `${tableName}.id` - if (sort && Object.keys(sort).length > 0) { - const sortClause = buildSortClause(sort, tableName, columns) - if (sortClause) { - return sql.join([sortClause, sql.raw(primary), sql.raw(id)], sql.raw(', ')) - } - } - return sql.raw(`${primary}, ${id}`) -} - -/** One matching cell from {@link findRowMatches}. */ -export interface FindRowMatch { - /** 0-based index of the row in the filtered+sorted view (aligns with the list query). */ - ordinal: number - rowId: string - /** Stable column id of the matching cell (the JSONB storage key), not the display name. */ - column: string -} - -/** Max matching cells returned by {@link findRowMatches}; one extra is fetched to detect truncation. */ -const FIND_MATCH_LIMIT = 1000 - -/** - * Case-insensitive substring search across every cell of a table's rows. Each - * matching cell becomes a {@link FindRowMatch} carrying its row id, column, and - * 0-based ordinal in the filtered+sorted view (so the client can page up to and - * reveal it). `filter`/`sort` mirror the active list view via - * {@link buildRowOrderBySql}, keeping ordinals aligned. - * - * Cost: one pass over the table's rows — `ILIKE` over `jsonb_each_text` cannot - * use the JSONB GIN index, and the ordinal's `row_number()` needs every row - * counted regardless. The planner can't estimate the lateral ILIKE (jsonb is - * opaque to it), so left alone it seq-scans the entire shared relation and - * disk-sorts the window input (measured 75s on a 1M-row table in a 12M-row - * relation). `SET LOCAL` planner flags keep it tenant-bounded; on the default - * order they additionally force the streaming `(table_id, order_key, id)` index - * walk where `row_number()` needs no sort at all (measured 2s). A `pg_trgm` GIN - * index on a text projection is the future accelerator if needed. - */ -export async function findRowMatches( - table: TableDefinition, - options: { q: string; filter?: Filter; sort?: Sort }, - requestId: string -): Promise<{ matches: FindRowMatch[]; truncated: boolean }> { - const tableName = USER_TABLE_ROWS_SQL_NAME - const columns = table.schema.columns - // Row data is keyed by stable column id, so scan/return JSONB keys as ids. - const columnIds = columns.map(getColumnId) - if (columnIds.length === 0) return { matches: [], truncated: false } - - // Same visibility rule as queryRows: don't surface rows a running delete job will remove. - const deleteMask = await pendingDeleteMask(table) - - const baseConditions = and( - eq(userTableRows.tableId, table.id), - eq(userTableRows.workspaceId, table.workspaceId), - deleteMask - ) - let whereClause: SQL | undefined = baseConditions - if (options.filter && Object.keys(options.filter).length > 0) { - const filterClause = buildFilterClause(options.filter, tableName, columns) - if (filterClause) whereClause = and(baseConditions, filterClause) - } - - const orderBySql = buildRowOrderBySql(options.sort, tableName, columns) - const pattern = `%${escapeLikePattern(options.q)}%` - - const result = await db.transaction(async (trx) => { - // Planner flags, not correctness: `enable_* = off` only penalizes a plan shape, so a - // genuinely required sort still runs. Seqscan off keeps the scan inside the tenant's rows - // (the lateral ILIKE is unestimatable, so the planner otherwise walks the whole shared - // relation). On the default order, the remaining flags steer to the already-sorted - // `(table_id, order_key, id)` index walk so the window function streams without a 100MB+ - // disk sort; a custom sort has no index to stream from, so those flags would only distort - // that plan. - await trx.execute(sql`SET LOCAL enable_seqscan = off`) - if (!options.sort) { - await trx.execute(sql`SET LOCAL enable_bitmapscan = off`) - await trx.execute(sql`SET LOCAL enable_sort = off`) - await trx.execute(sql`SET LOCAL max_parallel_workers_per_gather = 0`) - } - return trx.execute<{ - ordinal: string | number - id: string - column_name: string - }>(sql` - WITH ordered AS ( - SELECT id, data, row_number() OVER (ORDER BY ${orderBySql}) - 1 AS ordinal - FROM ${userTableRows} - WHERE ${whereClause} - ) - SELECT o.ordinal, o.id, kv.key AS column_name - FROM ordered o - CROSS JOIN LATERAL jsonb_each_text(o.data) kv - WHERE kv.value ILIKE ${pattern} - AND ${inArray(sql`kv.key`, columnIds)} - ORDER BY o.ordinal - LIMIT ${FIND_MATCH_LIMIT + 1} - `) - }) - - const all = Array.from(result) - const truncated = all.length > FIND_MATCH_LIMIT - const sliced = truncated ? all.slice(0, FIND_MATCH_LIMIT) : all - const matches: FindRowMatch[] = sliced.map((r) => ({ - ordinal: Number(r.ordinal), - rowId: r.id, - column: r.column_name, - })) - - logger.info( - `[${requestId}] Find "${options.q}" in table ${table.id}: ${matches.length} match(es)${truncated ? ' (truncated)' : ''}` - ) - - return { matches, truncated } -} - -/** - * Queries rows from a table with filtering, sorting, and pagination. - * - * Filter cost model: equality filters (`$eq`, `$in`) compile to JSONB - * containment (`@>`) and hit the GIN (jsonb_path_ops) index on - * `user_table_rows.data`. Range operators (`$gt`, `$gte`, `$lt`, `$lte`) and - * `$contains` compile to `data->>'field'` text extraction and bypass the GIN - * index — they fall back to a sequential scan of the rows for the table - * (bounded only by the btree on `table_id`). Prefer equality on hot paths; set - * `includeTotal: false` when the caller does not need the `COUNT(*)`. - * - * @param table - Table definition (provides id, workspaceId, and column schema for type-aware filter/sort casts) - * @param options - Query options (filter, sort, limit, offset) - * @param requestId - Request ID for logging - * @returns Query result with rows and pagination info - */ -/** - * Visibility mask for a running delete job: returns a clause keeping only rows the job will NOT - * delete, or `undefined` when no delete job is running. The job's persisted scope - * ({@link TableDeleteJobPayload}) defines the doomed set — `matches(filter) AND created_at <= - * cutoff AND id NOT IN excludeRowIds` — exactly what the worker's `selectRowIdPage` selects, so - * mid-job reads (refresh, other clients, exports) are consistent with the eventual result. The - * mask lifts automatically when the job leaves `running` (done, failed, or canceled). - * - * `(doomed) IS NOT TRUE` rather than `NOT (doomed)`: JSONB predicates evaluate to NULL on missing - * cells, and those rows are NOT selected for deletion (NULL ≠ TRUE) — they must stay visible. - */ -async function pendingDeleteMask(table: TableDefinition): Promise { - const [job] = await db - .select({ payload: tableJobs.payload }) - .from(tableJobs) - .where( - and( - eq(tableJobs.tableId, table.id), - eq(tableJobs.status, 'running'), - eq(tableJobs.type, 'delete') - ) - ) - .limit(1) - if (!job?.payload) return undefined - const scope = job.payload as TableDeleteJobPayload - - const doomedParts: SQL[] = [] - if (scope.filter && Object.keys(scope.filter).length > 0) { - try { - const clause = buildFilterClause(scope.filter, USER_TABLE_ROWS_SQL_NAME, table.schema.columns) - if (clause) doomedParts.push(clause) - } catch (error) { - // Schema drifted mid-job (column renamed/deleted). Showing doomed rows briefly beats - // failing every read; the worker resolves the same way on its next page. - logger.warn(`Skipping delete-job mask for table ${table.id}: stale filter`, { - error: toError(error).message, - }) - return undefined - } - } - if (scope.cutoff) doomedParts.push(lte(userTableRows.createdAt, new Date(scope.cutoff))) - if (scope.excludeRowIds && scope.excludeRowIds.length > 0) { - doomedParts.push(notInArray(userTableRows.id, scope.excludeRowIds)) - } - if (doomedParts.length === 0) return undefined - return sql`(${and(...doomedParts)}) IS NOT TRUE` -} - -/** - * `COUNT(*)` for a filtered view, kept inside the tenant's rows: measured - * 12.7s → 1.0s counting a rare ILIKE filter on a 1M-row table inside a 12M-row - * relation (see {@link withSeqscanOff} for why the planner gets this wrong). - */ -async function countRowsTenantBounded(whereClause: SQL | undefined): Promise { - return withSeqscanOff(async (trx) => { - const [result] = await trx.select({ count: count() }).from(userTableRows).where(whereClause) - return Number(result.count) - }) -} - -export async function queryRows( - table: TableDefinition, - options: QueryOptions, - requestId: string -): Promise { - const { - filter, - sort, - limit = TABLE_LIMITS.DEFAULT_QUERY_LIMIT, - offset = 0, - after, - includeTotal = true, - withExecutions = true, - } = options - - const tableName = USER_TABLE_ROWS_SQL_NAME - const columns = table.schema.columns - - // Hide rows a running delete job is about to remove — both the page and the count below share - // this clause, so totals stay consistent with the visible rows. - const deleteMask = await pendingDeleteMask(table) - - const baseConditions = and( - eq(userTableRows.tableId, table.id), - eq(userTableRows.workspaceId, table.workspaceId), - deleteMask - ) - - let whereClause = baseConditions - if (filter && Object.keys(filter).length > 0) { - const filterClause = buildFilterClause(filter, tableName, columns) - if (filterClause) { - whereClause = and(baseConditions, filterClause) - } - } - - // Keyset page: seek past the cursor on the default `(order_key, id)` order instead of paying - // OFFSET's scan-and-discard of every prior row (O(N²) across a deep scroll / full drain). Only - // valid without a custom sort — the contract rejects `after` + `sort` together. The count below - // deliberately excludes the cursor: totals cover the whole view, not the remaining pages. - const pageWhere = - after && !sort - ? and( - whereClause, - sql`(${userTableRows.orderKey}, ${userTableRows.id}) > (${after.orderKey}, ${after.id})` - ) - : whereClause - - const buildPageQuery = (executor: DbExecutor) => { - const query = executor - .select() - .from(userTableRows) - .where(pageWhere ?? baseConditions) - .orderBy(buildRowOrderBySql(sort, tableName, columns)) - return after ? query.limit(limit) : query.limit(limit).offset(offset) - } - - // Count and page fetch are independent reads — run them concurrently so the - // `includeTotal` hot path doesn't pay two serial round-trips. Filtered counts - // go through the tenant-bounded variant (see countRowsTenantBounded); the - // unfiltered count already plans an index-only scan on the table_id prefix. - // Custom column sorts order by `data->>'col'` — unestimatable, so left alone - // the planner seq-scans and sorts the whole shared relation on every page - // (9.7s measured on a 1M-row table; 0.76s tenant-bounded). Default-order - // pages already stream the `(table_id, order_key, id)` index. - const hasFilter = Boolean(filter && Object.keys(filter).length > 0) - const rowsPromise = sort ? withSeqscanOff(async (trx) => buildPageQuery(trx)) : buildPageQuery(db) - const countPromise = includeTotal - ? hasFilter - ? countRowsTenantBounded(whereClause) - : db - .select({ count: count() }) - .from(userTableRows) - .where(whereClause ?? baseConditions) - .then((r) => Number(r[0].count)) - : null - - const [rows, totalCount] = await Promise.all([rowsPromise, countPromise]) - - const executionsByRow = withExecutions - ? await loadExecutionsByRow( - db, - rows.map((r) => r.id) - ) - : null - - logger.info( - `[${requestId}] Queried ${rows.length} rows from table ${table.id} (total: ${totalCount})` - ) - - return { - rows: rows.map((r) => ({ - id: r.id, - data: r.data as RowData, - executions: executionsByRow?.get(r.id) ?? {}, - position: r.position, - orderKey: r.orderKey ?? undefined, - createdAt: r.createdAt, - updatedAt: r.updatedAt, - })), - rowCount: rows.length, - totalCount, - limit, - offset, - } -} - -/** - * Gets a single row by ID. - * - * @param tableId - Table ID - * @param rowId - Row ID to fetch - * @param workspaceId - Workspace ID for access control - * @returns Row or null if not found - */ -export async function getRowById( - tableId: string, - rowId: string, - workspaceId: string -): Promise { - const results = await db - .select() - .from(userTableRows) - .where( - and( - eq(userTableRows.id, rowId), - eq(userTableRows.tableId, tableId), - eq(userTableRows.workspaceId, workspaceId) - ) - ) - .limit(1) - - if (results.length === 0) return null - - const row = results[0] - const executions = await loadExecutionsForRow(db, row.id) - return { - id: row.id, - data: row.data as RowData, - executions, - position: row.position, - orderKey: row.orderKey ?? undefined, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - } -} - -/** - * Derive automatic clears + cancellation candidates from a row's data patch. - * - * Walks `schema.workflowGroups` left-to-right with a propagating `dirtied` - * column set. For each group whose deps overlap the dirty set, decide to - * clear (terminal exec) or cancel+rerun (in-flight exec), then add the - * group's outputs to the dirty set so later groups in the chain see them - * as dirty too. This models transitive dep chains as a single forward pass — - * editing column A propagates through group 1 (deps on A) to group 2 (deps - * on group 1's output) without explicit DAG traversal. - * - * Returns: - * - `executionsPatch`: caller's patch + nulls for cleared groups (or - * undefined if nothing applied). - * - `inFlightDownstreamGroups`: groups whose dep was dirtied and that are - * currently in-flight. Cancel-and-restart is the caller's job. - * - * Assumption: `workflowGroups[]` is in topological order — a group's deps - * may only reference columns to its left (enforced by `workflow-sidebar`'s - * "Run after" picker + the reorder scrub via `stripGroupDeps`). Violating - * this would silently miss the propagation. - */ -function deriveExecClearsForDataPatch( - dataPatch: RowData, - schema: TableSchema, - existingExecutions: RowExecutions, - callerPatch: Record | undefined, - mergedData: RowData -): { - executionsPatch: Record | undefined - inFlightDownstreamGroups: string[] -} { - const dirtied = new Set(Object.keys(dataPatch)) - const groupsToClear = new Set() - const inFlightDownstreamGroups: string[] = [] - - // Own-output clears: when the user wipes a workflow output column, drop - // that group's exec entry so the auto-fire reactor re-arms the cell. - // Also flags the cleared output column as dirty so transitive downstream - // groups see it. - for (const [columnId, value] of Object.entries(dataPatch)) { - const cleared = value === null || value === undefined || value === '' - if (!cleared) continue - const col = schema.columns.find((c) => getColumnId(c) === columnId) - if (col?.workflowGroupId) groupsToClear.add(col.workflowGroupId) - } - - // Left-to-right walk, propagating dirty columns forward. - const groups = schema.workflowGroups ?? [] - const afterRow = { data: mergedData } as TableRow - for (const group of groups) { - const deps = group.dependencies?.columns ?? [] - const depMatched = deps.some((d) => dirtied.has(d)) - if (!depMatched) continue - - // A dep column changed, but if the group's deps are no longer satisfied - // after the patch — a checkbox was unchecked or a text dep cleared — there's - // nothing to recompute. Leave the prior result alone instead of re-arming or - // cancelling it; only checking a box / filling a dep drives downstream work. - if (!areGroupDepsSatisfied(group, afterRow)) continue - - const exec = existingExecutions[group.id] - if (exec) { - const status = exec.status - if (status === 'completed' || status === 'error' || status === 'cancelled') { - groupsToClear.add(group.id) - } else if (status === 'queued' || status === 'running' || status === 'pending') { - inFlightDownstreamGroups.push(group.id) - } - } else { - // No exec entry yet — `mode: 'new'` already covers this group. We - // still propagate the dirty signal forward so later groups in the - // chain see this group's outputs as dirty too. - groupsToClear.add(group.id) - } - - // Propagate: this group is about to be re-computed, so groups whose - // deps reference its output columns are also dirty. - for (const out of group.outputs) dirtied.add(out.columnName) - } - - if (groupsToClear.size === 0) { - return { executionsPatch: callerPatch, inFlightDownstreamGroups } - } - const merged: Record = { ...(callerPatch ?? {}) } - for (const gid of groupsToClear) { - if (!(gid in merged)) merged[gid] = null - } - return { executionsPatch: merged, inFlightDownstreamGroups } -} - -/** Internal: thrown inside `db.transaction` to roll back when the executions - * guard rejects a write. The outer `.catch` translates it into a `null` return. */ -class GuardRejected extends Error { - constructor() { - super('cell-write guard rejected') - } -} - -/** Merges an `executionsPatch` into the row's existing executions blob. */ -function applyExecutionsPatch( - existing: RowExecutions, - patch: Record | undefined -): RowExecutions { - if (!patch) return existing - const next: RowExecutions = { ...existing } - for (const [gid, value] of Object.entries(patch)) { - if (value === null) { - delete next[gid] - } else { - next[gid] = value - } - } - return next -} - -/** - * Writes a per-group execution patch for one row against the `tableRowExecutions` - * sidecar. Non-null values upsert into the table; nulls delete the entry. When - * `guard` is set, the upsert is gated to: - * - reject if a `cancelled` row for the same execution already exists, and - * - reject if the row exists but is owned by a different executionId - * (with carve-outs for missing rows and null executionIds — the dispatcher's - * pre-batch `pending` stamp leaves executionId unset so the first cell-task - * can claim). - * - * Returns `'guard-rejected'` when the guarded group's upsert affected 0 rows - * (callers signal failure to the cell-task path). Returns `'wrote'` otherwise. - */ -async function writeExecutionsPatch( - trx: DbOrTx, - tableId: string, - rowId: string, - patch: Record | undefined, - guard?: { groupId: string; executionId: string } -): Promise<'wrote' | 'guard-rejected'> { - if (!patch) return 'wrote' - const entries = Object.entries(patch) - if (entries.length === 0) return 'wrote' - - for (const [gid, value] of entries) { - if (value === null) { - await trx - .delete(tableRowExecutions) - .where(and(eq(tableRowExecutions.rowId, rowId), eq(tableRowExecutions.groupId, gid)) as SQL) - continue - } - const insertValues = { - tableId, - rowId, - groupId: gid, - status: value.status, - executionId: value.executionId, - jobId: value.jobId, - workflowId: value.workflowId, - error: value.error, - runningBlockIds: value.runningBlockIds ?? [], - blockErrors: value.blockErrors ?? {}, - cancelledAt: value.cancelledAt ? new Date(value.cancelledAt) : null, - updatedAt: new Date(), - } as const - - const isGuarded = guard && guard.groupId === gid - if (isGuarded) { - // Gate by guard semantics. The original JSONB guard had two AND'd - // clauses; we collapse them onto the upsert's WHERE so a non-matching - // existing row leaves the table untouched and we observe 0 affected. - const guardExecutionId = guard.executionId - const updated = await trx - .insert(tableRowExecutions) - .values(insertValues) - .onConflictDoUpdate({ - target: [tableRowExecutions.rowId, tableRowExecutions.groupId], - set: { - status: insertValues.status, - executionId: insertValues.executionId, - jobId: insertValues.jobId, - workflowId: insertValues.workflowId, - error: insertValues.error, - runningBlockIds: insertValues.runningBlockIds, - blockErrors: insertValues.blockErrors, - cancelledAt: insertValues.cancelledAt, - updatedAt: insertValues.updatedAt, - }, - where: and( - // Reject any guarded worker write when the cell is `cancelled` — a - // stop click wrote it authoritatively. SQL mirror of `isExecCancelled` - // (deps.ts). Status-only (not executionId-scoped): the cancel can - // only carry the pre-stamp's executionId (often null), so matching on - // id would let the worker's real-id claim resurrect a killed cell. - sql`${tableRowExecutions.status} <> 'cancelled'`, - // Stale-worker: the cell's active run has moved on. Carve-outs - // permit a fresh worker to take over when the row's executionId - // is unset (dispatcher's pre-batch `pending` stamp). - sql`(${tableRowExecutions.executionId} IS NULL OR ${tableRowExecutions.executionId} = ${guardExecutionId})` - ) as SQL, - }) - .returning({ rowId: tableRowExecutions.rowId }) - if (updated.length === 0) return 'guard-rejected' - continue - } - - await trx - .insert(tableRowExecutions) - .values(insertValues) - .onConflictDoUpdate({ - target: [tableRowExecutions.rowId, tableRowExecutions.groupId], - set: { - status: insertValues.status, - executionId: insertValues.executionId, - jobId: insertValues.jobId, - workflowId: insertValues.workflowId, - error: insertValues.error, - runningBlockIds: insertValues.runningBlockIds, - blockErrors: insertValues.blockErrors, - cancelledAt: insertValues.cancelledAt, - updatedAt: insertValues.updatedAt, - }, - }) - } - - return 'wrote' -} - -/** - * Strips the given workflow group ids from every row's executions on a table — - * used by the column / group delete paths so stale running/queued exec records - * don't linger and inflate counters after the group is gone. The caller wraps - * in their own transaction. - */ -async function stripGroupExecutions( - trx: DbOrTx, - tableId: string, - groupIds: Iterable -): Promise { - const ids = Array.from(new Set(groupIds)) - if (ids.length === 0) return - await trx - .delete(tableRowExecutions) - .where( - and(eq(tableRowExecutions.tableId, tableId), inArray(tableRowExecutions.groupId, ids)) as SQL - ) -} - -/** - * Updates a single row. - * - * @param data - Update data - * @param table - Table definition - * @param requestId - Request ID for logging - * @returns Updated row - * @throws Error if row not found or validation fails - */ -export async function updateRow( - data: UpdateRowData, - table: TableDefinition, - requestId: string -): Promise { - // Get existing row - const existingRow = await getRowById(data.tableId, data.rowId, data.workspaceId) - if (!existingRow) { - throw new Error('Row not found') - } - - // Merge partial update with existing row data so callers can pass only changed fields - const mergedData = { - ...(existingRow.data as RowData), - ...data.data, - } - // Auto-clear exec records for workflow output columns the user just wiped - // AND for downstream groups whose deps just changed. Surfaces the in-flight - // downstream groups so the caller can cancel + re-run them. - const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = - deriveExecClearsForDataPatch( - data.data, - table.schema, - existingRow.executions, - data.executionsPatch, - mergedData - ) - const mergedExecutions = applyExecutionsPatch(existingRow.executions, effectiveExecutionsPatch) - - // Validate size - const sizeValidation = validateRowSize(mergedData) - if (!sizeValidation.valid) { - throw new Error(sizeValidation.errors.join(', ')) - } - - // Validate against schema - const schemaValidation = coerceRowToSchema(mergedData, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) - } - - // Check unique constraints using optimized database query - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0) { - const uniqueValidation = await checkUniqueConstraintsDb( - data.tableId, - mergedData, - table.schema, - data.rowId // Exclude current row - ) - if (!uniqueValidation.valid) { - throw new Error(uniqueValidation.errors.join(', ')) - } - } - - const now = new Date() - - // Cell-task partial writes pass `cancellationGuard` so the upsert into - // `tableRowExecutions` is a no-op when (a) a stop click already wrote - // `cancelled` for this run, or (b) a newer run has taken over the cell - // with a different executionId. Authoritative cancel writes from - // `cancelWorkflowGroupRuns` skip the guard entirely. Data + executions - // commit in one transaction so a partial write can't leave the sidecar - // and the row out of sync. - const guard = data.cancellationGuard - const guardRejected = await db - .transaction(async (trx) => { - await trx - .update(userTableRows) - .set({ data: mergedData, updatedAt: now }) - .where(eq(userTableRows.id, data.rowId)) - - const result = await writeExecutionsPatch( - trx, - data.tableId, - data.rowId, - effectiveExecutionsPatch, - guard - ) - if (result === 'guard-rejected') { - // Roll back the data update too — the worker isn't authoritative. - throw new GuardRejected() - } - return false - }) - .catch((err) => { - if (err instanceof GuardRejected) return true - throw err - }) - - if (guardRejected) { - return null - } - - logger.info(`[${requestId}] Updated row ${data.rowId} in table ${data.tableId}`) - - const updatedRow: TableRow = { - id: data.rowId, - data: mergedData, - executions: mergedExecutions, - position: existingRow.position, - createdAt: existingRow.createdAt, - updatedAt: now, - } - - const oldRows = new Map([[data.rowId, existingRow.data as RowData]]) - void fireTableTrigger( - data.tableId, - table.name, - 'update', - [updatedRow], - oldRows, - table.schema, - requestId - ) - - // Auto-fire only on user-facing data edits. Internal callers that mutate - // executions (cell-task partial/terminal writes, cancel writes) always pass - // `executionsPatch` — re-dispatching from those would recursively spawn new - // dispatches for every running/terminal write, flooding the dispatcher with - // redundant pre-stamps that strand `pending` cells. - const isInternalExecWrite = data.executionsPatch && Object.keys(data.executionsPatch).length > 0 - if (isInternalExecWrite) { - return updatedRow - } - - // Two passes: - // 1. Cancel in-flight downstream groups whose dep just changed, then - // manually re-run them — the cancel writes `cancelled` per cell and - // `mode: 'incomplete' + isManualRun: true` wipes those entries and - // re-enqueues. - // 2. `mode: 'new'` for groups that just had their exec entries cleared - // (own-output wipe OR terminal downstream dep-changed) — the - // dispatcher's `jsonb_exists_all` SQL filter lets the row through - // because at least one targeted group's exec is now missing. - if (inFlightDownstreamGroups.length > 0) { - void (async () => { - try { - await cancelWorkflowGroupRuns(data.tableId, data.rowId, { - groupIds: inFlightDownstreamGroups, - }) - await runWorkflowColumn({ - tableId: data.tableId, - workspaceId: data.workspaceId, - mode: 'incomplete', - isManualRun: true, - rowIds: [data.rowId], - groupIds: inFlightDownstreamGroups, - requestId, - triggeredByUserId: data.actorUserId, - }) - } catch (err) { - logger.error(`[${requestId}] cancel+rerun for in-flight downstream groups failed:`, err) - } - })() - } - void runWorkflowColumn({ - tableId: data.tableId, - workspaceId: data.workspaceId, - rowIds: [data.rowId], - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: data.actorUserId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRow) failed:`, err)) - - return updatedRow -} - -/** - * Deletes a single row (hard delete). - * - * @param tableId - Table ID - * @param rowId - Row ID to delete - * @param workspaceId - Workspace ID for access control - * @param requestId - Request ID for logging - * @throws Error if row not found - */ -export async function deleteRow( - tableId: string, - rowId: string, - workspaceId: string, - requestId: string -): Promise { - const deleted = await deleteOrderedRow({ tableId, rowId, workspaceId }) - if (!deleted) throw new Error('Row not found') - - logger.info(`[${requestId}] Deleted row ${rowId} from table ${tableId}`) -} - -/** - * Updates multiple rows matching a filter. - * - * @param table - Table definition (provides column schema for type-aware filter casts) - * @param data - Bulk update data - * @param requestId - Request ID for logging - * @returns Bulk operation result - */ -export async function updateRowsByFilter( - table: TableDefinition, - data: BulkUpdateData, - requestId: string -): Promise { - const tableName = USER_TABLE_ROWS_SQL_NAME - - const filterClause = buildFilterClause(data.filter, tableName, table.schema.columns) - if (!filterClause) { - throw new Error('Filter is required for bulk update') - } - - const baseConditions = and( - eq(userTableRows.tableId, table.id), - eq(userTableRows.workspaceId, table.workspaceId) - ) - - // Tenant-bounded: the jsonb filter is unestimatable and otherwise sends the planner to a - // whole-shared-relation seq scan (14.4s measured on a 1M-row table). - const matchingRows = await withSeqscanOff(async (trx) => { - let query = trx - .select({ id: userTableRows.id, data: userTableRows.data }) - .from(userTableRows) - .where(and(baseConditions, filterClause)) - if (data.limit) { - query = query.limit(data.limit) as typeof query - } - return query - }) - - if (matchingRows.length === 0) { - return { affectedCount: 0, affectedRowIds: [] } - } - - // Coerce the patch itself in place — the write below persists `data.data` - // (as `patchJson`), so coercing only the per-row merged copies would be - // discarded. The merged validation in the loop still enforces required - // fields against the full row. - coerceRowValues(data.data, table.schema) - - for (const row of matchingRows) { - const existingData = row.data as RowData - const mergedData = { ...existingData, ...data.data } - - const sizeValidation = validateRowSize(mergedData) - if (!sizeValidation.valid) { - throw new Error(`Row ${row.id}: ${sizeValidation.errors.join(', ')}`) - } - - const schemaValidation = coerceRowToSchema(mergedData, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Row ${row.id}: ${schemaValidation.errors.join(', ')}`) - } - } - - const uniqueColumns = getUniqueColumns(table.schema) - const uniqueColumnsInUpdate = uniqueColumns.filter((col) => col.name in data.data) - if (uniqueColumnsInUpdate.length > 0) { - if (matchingRows.length > 1) { - throw new Error( - `Cannot set unique column values when updating multiple rows. ` + - `Columns with unique constraint: ${uniqueColumnsInUpdate.map((c) => c.name).join(', ')}. ` + - `Updating ${matchingRows.length} rows with the same value would violate uniqueness.` - ) - } - - // Only one row — only the touched unique columns need re-checking. - const row = matchingRows[0] - const mergedData = { ...(row.data as RowData), ...data.data } - const uniqueValidation = await checkUniqueConstraintsDb( - table.id, - mergedData, - table.schema, - row.id - ) - if (!uniqueValidation.valid) { - throw new Error(`Unique constraint violation: ${uniqueValidation.errors.join(', ')}`) - } - } - - const now = new Date() - const ids = matchingRows.map((r) => r.id) - const patchJson = JSON.stringify(data.data) - - await db.transaction(async (trx) => { - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - for (let i = 0; i < ids.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { - const batchIds = ids.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) - await trx - .update(userTableRows) - .set({ - data: sql`${userTableRows.data} || ${patchJson}::jsonb`, - updatedAt: now, - }) - .where(inArray(userTableRows.id, batchIds)) - } - }) - - logger.info(`[${requestId}] Updated ${matchingRows.length} rows in table ${table.id}`) - - const oldRows = new Map(matchingRows.map((r) => [r.id, r.data as RowData])) - const updatedRows: TableRow[] = matchingRows.map((r) => ({ - id: r.id, - data: { ...(r.data as RowData), ...data.data }, - executions: {}, - position: 0, - createdAt: now, - updatedAt: now, - })) - void fireTableTrigger( - table.id, - table.name, - 'update', - updatedRows, - oldRows, - table.schema, - requestId - ) - void runWorkflowColumn({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds: updatedRows.map((r) => r.id), - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: data.actorUserId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRowsByFilter) failed:`, err)) - - return { - affectedCount: matchingRows.length, - affectedRowIds: ids, - } -} - -/** - * Updates multiple rows with per-row data in a single transaction. - * Avoids the race condition of parallel update_row calls overwriting each other. - */ -export async function batchUpdateRows( - data: BatchUpdateByIdData, - table: TableDefinition, - requestId: string -): Promise { - if (data.updates.length === 0) { - return { affectedCount: 0, affectedRowIds: [] } - } - - const rowIds = data.updates.map((u) => u.rowId) - const existingRows = await db - .select({ - id: userTableRows.id, - data: userTableRows.data, - }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, data.tableId), - eq(userTableRows.workspaceId, data.workspaceId), - inArray(userTableRows.id, rowIds) - ) - ) - - const executionsByRow = await loadExecutionsByRow( - db, - existingRows.map((r) => r.id) - ) - - type ExistingRow = { data: RowData; executions: RowExecutions } - const existingMap = new Map( - existingRows.map((r) => [ - r.id, - { data: r.data as RowData, executions: executionsByRow.get(r.id) ?? {} }, - ]) - ) - - const missing = rowIds.filter((id) => !existingMap.has(id)) - if (missing.length > 0) { - throw new Error(`Rows not found: ${missing.join(', ')}`) - } - - const mergedUpdates: Array<{ - rowId: string - mergedData: RowData - mergedExecutions: RowExecutions - executionsPatch?: Record - inFlightDownstreamGroups: string[] - }> = [] - for (const update of data.updates) { - const existing = existingMap.get(update.rowId)! - const merged = { ...existing.data, ...update.data } - // Auto-clear exec records for workflow output columns the user just - // wiped AND downstream dep-changed terminal groups — same rationale as - // `updateRow`. Per-row in-flight downstream groups are surfaced so we - // can run the cancel+rerun orchestration after the batch commits. - const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = - deriveExecClearsForDataPatch( - update.data, - table.schema, - existing.executions, - update.executionsPatch, - merged - ) - const mergedExecutions = applyExecutionsPatch(existing.executions, effectiveExecutionsPatch) - - const sizeValidation = validateRowSize(merged) - if (!sizeValidation.valid) { - throw new Error(`Row ${update.rowId}: ${sizeValidation.errors.join(', ')}`) - } - - const schemaValidation = coerceRowToSchema(merged, table.schema) - if (!schemaValidation.valid) { - throw new Error(`Row ${update.rowId}: ${schemaValidation.errors.join(', ')}`) - } - - mergedUpdates.push({ - rowId: update.rowId, - mergedData: merged, - mergedExecutions, - executionsPatch: effectiveExecutionsPatch, - inFlightDownstreamGroups, - }) - } - - const uniqueColumns = getUniqueColumns(table.schema) - if (uniqueColumns.length > 0) { - for (const { rowId, mergedData } of mergedUpdates) { - const uniqueValidation = await checkUniqueConstraintsDb( - data.tableId, - mergedData, - table.schema, - rowId - ) - if (!uniqueValidation.valid) { - throw new Error(`Row ${rowId}: ${uniqueValidation.errors.join(', ')}`) - } - } - } - - const now = new Date() - - await db.transaction(async (trx) => { - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - for (let i = 0; i < mergedUpdates.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { - const batch = mergedUpdates.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) - // Update row data in parallel; sidecar exec writes are sequential per - // row (each goes through writeExecutionsPatch's per-key upsert). - const dataPromises = batch.map(({ rowId, mergedData }) => - trx - .update(userTableRows) - .set({ data: mergedData, updatedAt: now }) - .where(eq(userTableRows.id, rowId)) - ) - await Promise.all(dataPromises) - for (const { rowId, executionsPatch } of batch) { - await writeExecutionsPatch(trx, data.tableId, rowId, executionsPatch) - } - } - }) - - logger.info(`[${requestId}] Batch updated ${mergedUpdates.length} rows in table ${data.tableId}`) - - const oldRowsForTrigger = new Map( - data.updates.map((u) => [u.rowId, existingMap.get(u.rowId)!.data]) - ) - const updatedRowsForTrigger: TableRow[] = mergedUpdates.map( - ({ rowId, mergedData, mergedExecutions }) => ({ - id: rowId, - data: mergedData, - executions: mergedExecutions, - position: 0, - createdAt: now, - updatedAt: now, - }) - ) - void fireTableTrigger( - data.tableId, - table.name, - 'update', - updatedRowsForTrigger, - oldRowsForTrigger, - table.schema, - requestId - ) - // Per-row cancel+rerun for in-flight downstream groups whose deps just - // changed — same orchestration as single-row `updateRow`. Without this, - // batch updates would leave running workflows reading stale dep values. - // Each row needs its own cancel + manual-incomplete dispatch because - // `cancelWorkflowGroupRuns`'s `groupIds` filter is per-row. - const rowsWithInFlightDownstream = mergedUpdates.filter( - (u) => u.inFlightDownstreamGroups.length > 0 - ) - if (rowsWithInFlightDownstream.length > 0) { - void (async () => { - try { - for (const { rowId, inFlightDownstreamGroups } of rowsWithInFlightDownstream) { - await cancelWorkflowGroupRuns(data.tableId, rowId, { - groupIds: inFlightDownstreamGroups, - }) - await runWorkflowColumn({ - tableId: data.tableId, - workspaceId: data.workspaceId, - mode: 'incomplete', - isManualRun: true, - rowIds: [rowId], - groupIds: inFlightDownstreamGroups, - requestId, - triggeredByUserId: data.actorUserId, - }) - } - } catch (err) { - logger.error( - `[${requestId}] cancel+rerun for in-flight downstream groups (batch) failed:`, - err - ) - } - })() - } - void runWorkflowColumn({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds: updatedRowsForTrigger.map((r) => r.id), - mode: 'new', - isManualRun: false, - requestId, - triggeredByUserId: data.actorUserId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchUpdateRows) failed:`, err)) - - return { - affectedCount: mergedUpdates.length, - affectedRowIds: mergedUpdates.map((u) => u.rowId), - } -} - -/** - * Deletes multiple rows matching a filter. - * - * @param table - Table definition (provides column schema for type-aware filter casts) - * @param data - Bulk delete data - * @param requestId - Request ID for logging - * @returns Bulk operation result - */ -export async function deleteRowsByFilter( - table: TableDefinition, - data: BulkDeleteData, - requestId: string -): Promise { - const tableName = USER_TABLE_ROWS_SQL_NAME - - // Build filter clause - const filterClause = buildFilterClause(data.filter, tableName, table.schema.columns) - if (!filterClause) { - throw new Error('Filter is required for bulk delete') - } - - // Find matching rows - const baseConditions = and( - eq(userTableRows.tableId, table.id), - eq(userTableRows.workspaceId, table.workspaceId) - ) - - // Tenant-bounded for the same reason as updateRowsByFilter — see withSeqscanOff. - const matchingRows = await withSeqscanOff(async (trx) => { - let query = trx - .select({ id: userTableRows.id, position: userTableRows.position }) - .from(userTableRows) - .where(and(baseConditions, filterClause)) - if (data.limit) { - query = query.limit(data.limit) as typeof query - } - return query - }) - - if (matchingRows.length === 0) { - return { affectedCount: 0, affectedRowIds: [] } - } - - const rowIds = matchingRows.map((r) => r.id) - - await deleteOrderedRowsByIds({ - tableId: table.id, - workspaceId: table.workspaceId, - rowIds, - }) - - logger.info(`[${requestId}] Deleted ${matchingRows.length} rows from table ${table.id}`) - - return { - affectedCount: matchingRows.length, - affectedRowIds: rowIds, - } -} - -/** - * Deletes rows by their IDs. - * - * @param data - Row IDs and table context - * @param requestId - Request ID for logging - * @returns Deletion result with deleted/missing row IDs - */ -export async function deleteRowsByIds( - data: BulkDeleteByIdsData, - requestId: string -): Promise { - const uniqueRequestedRowIds = Array.from(new Set(data.rowIds)) - - const deletedRows = await deleteOrderedRowsByIds({ - tableId: data.tableId, - workspaceId: data.workspaceId, - rowIds: uniqueRequestedRowIds, - }) - - const deletedIds = deletedRows.map((r) => r.id) - const deletedIdSet = new Set(deletedIds) - const missingRowIds = uniqueRequestedRowIds.filter((id) => !deletedIdSet.has(id)) - - logger.info(`[${requestId}] Deleted ${deletedIds.length} rows by ID from table ${data.tableId}`) - - return { - deletedCount: deletedIds.length, - deletedRowIds: deletedIds, - requestedCount: uniqueRequestedRowIds.length, - missingRowIds, - } -} - -/** - * Renames a column in a table's schema and updates all row data keys. - * - * @param data - Rename column data - * @param requestId - Request ID for logging - * @returns Updated table definition - * @throws Error if table not found, column not found, or new name conflicts - */ -export async function renameColumn( - data: RenameColumnData, - requestId: string -): Promise { - return withLockedTable(data.tableId, async (table, trx) => { - if (!NAME_PATTERN.test(data.newName)) { - throw new Error( - `Invalid column name "${data.newName}". Column names must start with a letter or underscore, followed by alphanumeric characters or underscores.` - ) - } - - if (data.newName.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { - throw new Error( - `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` - ) - } - - const schema = table.schema - const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.oldName)) - if (columnIndex === -1) { - throw new Error(`Column "${data.oldName}" not found`) - } - - if ( - schema.columns.some( - (c, i) => i !== columnIndex && c.name.toLowerCase() === data.newName.toLowerCase() - ) - ) { - throw new Error(`Column "${data.newName}" already exists`) - } - - const targetColumn = schema.columns[columnIndex] - const actualOldName = targetColumn.name - - // Rename is metadata-only: stored rows, metadata, and workflow-group refs all - // key on the column's stable id, which a rename never changes — so this is a - // pure schema write, no per-row JSONB rewrite or group/metadata cascade. - // Stamp the current storage key as the id (for any not-yet-backfilled column) - // so existing rows stay reachable as the display name changes. - const columnId = targetColumn.id ?? actualOldName - const updatedColumns = schema.columns.map((c, i) => - i === columnIndex ? { ...c, id: columnId, name: data.newName } : c - ) - const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } - assertValidSchema(updatedSchema, table.metadata?.columnOrder) - - const now = new Date() - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - logger.info( - `[${requestId}] Renamed column "${actualOldName}" to "${data.newName}" in table ${data.tableId}` - ) - return { ...table, schema: updatedSchema, updatedAt: now } - }) -} - -/** Removes the given column-id keys from a metadata blob (widths/order/pinned). */ -function stripColumnIdsFromMetadata( - metadata: TableMetadata | null, - ids: ReadonlySet -): TableMetadata | null { - if (!metadata) return metadata - let next = metadata - if (metadata.columnWidths) { - const widths = { ...metadata.columnWidths } - let changed = false - for (const id of ids) - if (id in widths) { - delete widths[id] - changed = true - } - if (changed) next = { ...next, columnWidths: widths } - } - if (metadata.columnOrder?.some((id) => ids.has(id))) { - next = { ...next, columnOrder: metadata.columnOrder.filter((id) => !ids.has(id)) } - } - if (metadata.pinnedColumns?.some((id) => ids.has(id))) { - next = { ...next, pinnedColumns: metadata.pinnedColumns.filter((id) => !ids.has(id)) } - } - return next -} - -/** - * Fire-and-forget reclamation of a deleted column's row storage. The column is - * already gone from the schema, so reads never surface the orphaned id — - * dropping the JSONB key just frees space. Runs in its own transaction with a - * row-count-scaled timeout; failures are logged, not propagated. - */ -function stripColumnDataInBackground( - tableId: string, - columnIds: string[], - rowCount: number, - requestId: string -): void { - if (columnIds.length === 0) return - void (async () => { - try { - await db.transaction(async (trx) => { - const statementMs = scaledStatementTimeoutMs(rowCount, { - baseMs: 60_000, - perRowMs: 2 * columnIds.length, - }) - await setTableTxTimeouts(trx, { statementMs }) - for (const id of columnIds) { - await trx.execute( - sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${tableId} AND data ? ${id}::text` - ) - } - }) - logger.info( - `[${requestId}] Background-stripped deleted column data [${columnIds.join(', ')}] from table ${tableId}` - ) - } catch (err) { - logger.error( - `[${requestId}] Background column-data strip failed for table ${tableId} [${columnIds.join(', ')}]:`, - err - ) - } - })() -} - -/** - * Deletes a column from a table's schema. When id-keyed, returns once the schema - * is updated and reclaims the column's row-data storage in the background - * (fire-and-forget); the legacy path strips the row key synchronously. - * - * @param data - Delete column data - * @param requestId - Request ID for logging - * @returns Updated table definition - * @throws Error if table not found, column not found, or it's the last column - */ -export async function deleteColumn( - data: DeleteColumnData, - requestId: string -): Promise { - const { def, stripKey } = await withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) - if (columnIndex === -1) { - throw new Error(`Column "${data.columnName}" not found`) - } - - if (schema.columns.length <= 1) { - throw new Error('Cannot delete the last column in a table') - } - - const targetColumn = schema.columns[columnIndex] - const actualName = targetColumn.name - const columnId = getColumnId(targetColumn) - const ownerGroupId = targetColumn.workflowGroupId - - // Drop this column's reference (by id) from every group's outputs and - // `columns` dependency. If the column is the last output of its parent - // group, the group itself is also removed (a group with zero outputs is - // invalid). - let groupRemovedId: string | null = null - const updatedGroups = (schema.workflowGroups ?? []) - .map((group) => { - let next = group - if (ownerGroupId && group.id === ownerGroupId) { - const remaining = group.outputs.filter((o) => o.columnName !== columnId) - if (remaining.length === 0) { - groupRemovedId = group.id - } - next = { ...next, outputs: remaining } - } - return stripGroupDeps(next, new Set([columnId])) - }) - .filter((g) => g.id !== groupRemovedId) - - const updatedSchema: TableSchema = { - ...schema, - columns: schema.columns.filter((_, i) => i !== columnIndex), - ...(updatedGroups.length > 0 ? { workflowGroups: updatedGroups } : {}), - } - const updatedMetadata = stripColumnIdsFromMetadata( - table.metadata as TableMetadata | null, - new Set([columnId]) - ) - assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) - - const now = new Date() - - // Schema/metadata update commits now; the column's row-data storage is - // reclaimed in the background (fire-and-forget) — reads never surface the - // orphaned id since the column is already gone from the schema. - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - if (groupRemovedId) await stripGroupExecutions(trx, data.tableId, [groupRemovedId]) - - logger.info(`[${requestId}] Deleted column "${actualName}" from table ${data.tableId}`) - - return { - def: { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }, - stripKey: columnId, - } - }) - - stripColumnDataInBackground(data.tableId, [stripKey], def.rowCount ?? 0, requestId) - return def -} - -/** - * Deletes multiple columns from a table in a single transaction. - * Avoids the race condition of calling deleteColumn multiple times in parallel. - */ -export async function deleteColumns( - data: { tableId: string; columnNames: string[] }, - requestId: string -): Promise { - const { def, stripKeys } = await withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const namesToDelete = new Set() - const idsToDelete = new Set() - const notFound: string[] = [] - - for (const name of data.columnNames) { - const col = schema.columns.find((c) => columnMatchesRef(c, name)) - if (!col) { - notFound.push(name) - } else { - namesToDelete.add(col.name) - idsToDelete.add(getColumnId(col)) - } - } - - if (notFound.length > 0) { - throw new Error(`Columns not found: ${notFound.join(', ')}`) - } - - const remaining = schema.columns.filter((c) => !namesToDelete.has(c.name)) - if (remaining.length === 0) { - throw new Error('Cannot delete all columns from a table') - } - - // For each group, drop outputs whose column (by id) is being deleted. Groups - // that end up with zero outputs are removed entirely (they'd be invalid). - // Then any remaining group's dependencies referencing a removed column are - // cleaned up. - const removedGroupIds = new Set() - let updatedGroups = (schema.workflowGroups ?? []).map((group) => { - const remainingOutputs = group.outputs.filter((o) => !idsToDelete.has(o.columnName)) - if (remainingOutputs.length === 0) { - removedGroupIds.add(group.id) - } - return remainingOutputs.length === group.outputs.length - ? group - : { ...group, outputs: remainingOutputs } - }) - updatedGroups = updatedGroups - .filter((g) => !removedGroupIds.has(g.id)) - .map((group) => stripGroupDeps(group, idsToDelete)) - const updatedSchema: TableSchema = { - ...schema, - columns: remaining, - ...(updatedGroups.length > 0 ? { workflowGroups: updatedGroups } : {}), - } - const updatedMetadata = stripColumnIdsFromMetadata( - table.metadata as TableMetadata | null, - idsToDelete - ) - assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) - - const now = new Date() - - // Schema/metadata commit now; row storage for the deleted columns is - // reclaimed in the background (fire-and-forget). - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - await stripGroupExecutions(trx, data.tableId, removedGroupIds) - - logger.info( - `[${requestId}] Deleted columns [${[...namesToDelete].join(', ')}] from table ${data.tableId}` - ) - - return { - def: { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }, - stripKeys: Array.from(idsToDelete), - } - }) - - if (stripKeys.length > 0) { - stripColumnDataInBackground(data.tableId, stripKeys, def.rowCount ?? 0, requestId) - } - return def -} - -/** - * Changes the type of a column. Validates that existing data is compatible. - * - * @param data - Update column type data - * @param requestId - Request ID for logging - * @returns Updated table definition - * @throws Error if table not found, column not found, or existing data is incompatible - */ -export async function updateColumnType( - data: UpdateColumnTypeData, - requestId: string -): Promise { - return withLockedTable(data.tableId, async (table, trx) => { - // Scale both statement and idle timeouts to row count: the compatibility - // check below iterates every row in Node between the row SELECT and the - // schema UPDATE, leaving the transaction idle for that gap. The default 5s - // `idle_in_transaction_session_timeout` would abort a valid type change on - // a large table. - const timeoutMs = scaledStatementTimeoutMs(table.rowCount ?? 0, { - baseMs: 60_000, - perRowMs: 2, - }) - await setTableTxTimeouts(trx, { statementMs: timeoutMs, idleMs: timeoutMs }) - - if (!(COLUMN_TYPES as readonly string[]).includes(data.newType)) { - throw new Error( - `Invalid column type "${data.newType}". Valid types: ${COLUMN_TYPES.join(', ')}` - ) - } - - const schema = table.schema - const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) - if (columnIndex === -1) { - throw new Error(`Column "${data.columnName}" not found`) - } - - const column = schema.columns[columnIndex] - if (column.type === data.newType) { - return table - } - const columnKey = getColumnId(column) - - // Validate existing data is compatible with the new type - const rows = await trx - .select({ id: userTableRows.id, data: userTableRows.data }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, data.tableId), - sql`${userTableRows.data} ? ${columnKey}`, - sql`${userTableRows.data}->>${columnKey}::text IS NOT NULL` - ) - ) - - let incompatibleCount = 0 - for (const row of rows) { - const rowData = row.data as RowData - const value = rowData[columnKey] - if (value === null || value === undefined) continue - - if (!isValueCompatibleWithType(value, data.newType)) { - incompatibleCount++ - } - } - - if (incompatibleCount > 0) { - throw new Error( - `Cannot change column "${column.name}" to type "${data.newType}": ${incompatibleCount} row(s) have incompatible values. Fix or remove the incompatible values first.` - ) - } - - const updatedColumns = schema.columns.map((c, i) => - i === columnIndex ? { ...c, type: data.newType } : c - ) - const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } - const now = new Date() - - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - logger.info( - `[${requestId}] Changed column "${column.name}" type from "${column.type}" to "${data.newType}" in table ${data.tableId}` - ) - - return { ...table, schema: updatedSchema, updatedAt: now } - }) -} - -/** - * Updates constraints (required, unique) on a column. - * - * @param data - Update column constraints data - * @param requestId - Request ID for logging - * @returns Updated table definition - * @throws Error if table not found, column not found, or existing data violates the constraint - */ -export async function updateColumnConstraints( - data: UpdateColumnConstraintsData, - requestId: string -): Promise { - return withLockedTable(data.tableId, async (table, trx) => { - // Scale both statement and idle timeouts to row count: the required/unique - // validation runs between separate queries inside this transaction, leaving - // it briefly idle. Match `updateColumnType` so the default 5s - // `idle_in_transaction_session_timeout` can't abort a valid change on a - // large table. - const timeoutMs = scaledStatementTimeoutMs(table.rowCount ?? 0, { - baseMs: 60_000, - perRowMs: 2, - }) - await setTableTxTimeouts(trx, { statementMs: timeoutMs, idleMs: timeoutMs }) - - const schema = table.schema - const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) - if (columnIndex === -1) { - throw new Error(`Column "${data.columnName}" not found`) - } - - const column = schema.columns[columnIndex] - const columnKey = getColumnId(column) - if (column.workflowGroupId) { - throw new Error( - `Cannot change constraints on workflow-output column "${column.name}". Constraints aren't applicable to columns whose values come from workflow execution.` - ) - } - if (data.required === true && !column.required) { - const [result] = await trx - .select({ count: count() }) - .from(userTableRows) - .where( - and( - eq(userTableRows.tableId, data.tableId), - sql`(NOT (${userTableRows.data} ? ${columnKey}) OR ${userTableRows.data}->>${columnKey}::text IS NULL)` - ) - ) - - if (result.count > 0) { - throw new Error( - `Cannot set column "${column.name}" as required: ${result.count} row(s) have null or missing values` - ) - } - } - - if (data.unique === true && !column.unique) { - const duplicates = (await trx.execute( - sql`SELECT ${userTableRows.data}->>${columnKey}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${data.tableId} AND ${userTableRows.data} ? ${columnKey} AND ${userTableRows.data}->>${columnKey}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1` - )) as { val: string; cnt: number }[] - - if (duplicates.length > 0) { - throw new Error(`Cannot set column "${column.name}" as unique: duplicate values exist`) - } - } - - const updatedColumns = schema.columns.map((c, i) => - i === columnIndex - ? { - ...c, - ...(data.required !== undefined ? { required: data.required } : {}), - ...(data.unique !== undefined ? { unique: data.unique } : {}), - } - : c - ) - const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } - const now = new Date() - - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - logger.info( - `[${requestId}] Updated constraints for column "${column.name}" in table ${data.tableId}` - ) - - return { ...table, schema: updatedSchema, updatedAt: now } - }) -} - -/** - * Atomically inserts a workflow group plus its output columns into a table's - * schema. Both arrays update in one DB write so the schema is never observed - * mid-mutation (e.g. columns referencing a group that doesn't yet exist). - */ -export async function addWorkflowGroup( - data: AddWorkflowGroupData, - requestId: string -): Promise { - const updatedTable = await withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const groups = schema.workflowGroups ?? [] - if (groups.some((g) => g.id === data.group.id)) { - throw new Error(`Workflow group "${data.group.id}" already exists`) - } - - const existingNames = new Set(schema.columns.map((c) => c.name.toLowerCase())) - for (const col of data.outputColumns) { - if (!NAME_PATTERN.test(col.name)) { - throw new Error( - `Invalid output column name "${col.name}". Must satisfy ${NAME_PATTERN.source}.` - ) - } - if (existingNames.has(col.name.toLowerCase())) { - throw new Error(`Column "${col.name}" already exists`) - } - } - - if (schema.columns.length + data.outputColumns.length > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { - throw new Error( - `Adding ${data.outputColumns.length} columns would exceed the maximum (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE}).` - ) - } - - // Assign stable ids to the new output columns, then rewrite the group's - // column refs from name → id so outputs/deps/inputMappings key on ids — - // matching the row-data storage key and surviving future renames. - const outputColumns = data.outputColumns.map((col) => - col.id ? col : { ...col, id: generateColumnId() } - ) - const updatedColumns = [...schema.columns, ...outputColumns] - const idByName = new Map(updatedColumns.map((c) => [c.name, getColumnId(c)])) - const group = remapGroupColumnRefs(data.group, idByName) - - const updatedSchema: TableSchema = { - ...schema, - columns: updatedColumns, - workflowGroups: [...groups, group], - } - - // Keep `metadata.columnOrder` (column ids) in sync — see `addTableColumn`. - // New output columns get appended in the order the caller supplied. - const existingOrder = table.metadata?.columnOrder - let updatedMetadata = table.metadata - if (existingOrder && existingOrder.length > 0) { - const known = new Set(existingOrder) - const append = outputColumns.map(getColumnId).filter((id) => !known.has(id)) - if (append.length > 0) { - updatedMetadata = { ...table.metadata, columnOrder: [...existingOrder, ...append] } - } - } - - assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) - - const now = new Date() - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - logger.info( - `[${requestId}] Added workflow group "${data.group.id}" with ${data.outputColumns.length} output column(s) to table ${data.tableId}` - ) - - return { - ...table, - schema: updatedSchema, - metadata: updatedMetadata, - updatedAt: now, - } - }) - - // Auto-fire existing rows whose deps are already met for the new group. - // Fire-and-forget — the dispatcher bounds queue depth (window of 20) and - // walks the table in the background. HTTP returns instantly; cells fill - // in over the next minutes as the dispatcher walks. Mothership opts out - // by setting `autoRun: false`. - if (data.autoRun !== false) { - void runWorkflowColumn({ - tableId: updatedTable.id, - workspaceId: updatedTable.workspaceId, - mode: 'new', - isManualRun: false, - groupIds: [data.group.id], - requestId, - triggeredByUserId: data.actorUserId, - }).catch((err) => logger.error(`[${requestId}] auto-dispatch (addWorkflowGroup) failed:`, err)) - } - - return updatedTable -} - -/** - * Updates a workflow group: any combination of workflowId, name, dependencies, - * outputs[]. Computes added/removed outputs vs current state and inserts / - * removes columns transactionally. Removed outputs also clear their key from - * every row's `data`. - */ -export async function updateWorkflowGroup( - data: UpdateWorkflowGroupData, - requestId: string -): Promise { - const mappingUpdates = data.mappingUpdates ?? [] - - // Phase 1 (no lock): when there are mapping updates, load the workflow once to - // resolve each remap's new leaf type. Kept OFF the advisory-lock critical - // section so concurrent group edits on the same table don't time out waiting - // on this DB load. Best-effort — a resolution failure leaves column types - // unchanged (workflow deleted, block removed). The result is applied against - // the fresh schema under the lock in phase 2. - const remapLeafTypeByColumn = new Map() - // The workflow id the leaf types above were resolved against. Phase 2 only - // applies the resolved types if the group still points at this workflow under - // the lock — a concurrent `workflowId` change would make them stale. - let resolvedForWorkflowId: string | undefined - if (mappingUpdates.length > 0) { - try { - const preTable = await getTableById(data.tableId) - const preGroup = preTable?.schema.workflowGroups?.find((g) => g.id === data.groupId) - const targetWorkflowId = data.workflowId ?? preGroup?.workflowId - if (targetWorkflowId) { - resolvedForWorkflowId = targetWorkflowId - const [ - { loadWorkflowFromNormalizedTables }, - { flattenWorkflowOutputs }, - { columnTypeForLeaf }, - ] = await Promise.all([ - import('@/lib/workflows/persistence/utils'), - import('@/lib/workflows/blocks/flatten-outputs'), - import('./column-naming'), - ]) - const normalized = await loadWorkflowFromNormalizedTables(targetWorkflowId) - if (normalized) { - const blocks = Object.values(normalized.blocks ?? {}).map((b) => ({ - id: b.id, - type: b.type, - name: b.name, - triggerMode: (b as { triggerMode?: boolean }).triggerMode, - subBlocks: b.subBlocks as Record | undefined, - })) - const flattened = flattenWorkflowOutputs(blocks, normalized.edges ?? []) - const flatByKey = new Map(flattened.map((f) => [`${f.blockId}::${f.path}`, f])) - for (const u of mappingUpdates) { - const match = flatByKey.get(`${u.blockId}::${u.path}`) - if (!match) continue - const newType = columnTypeForLeaf(match.leafType) - if (newType) remapLeafTypeByColumn.set(u.columnName, newType) - } - } - } - } catch (err) { - logger.warn( - `[${requestId}] Could not resolve new leaf types for remap on group ${data.groupId}; leaving column types unchanged:`, - err - ) - } - } - - const { updatedTable, added, remappedColumnIds, newOutputs, previousAutoRun } = - await withLockedTable(data.tableId, async (table, trx) => { - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - - const schema = table.schema - const groups = schema.workflowGroups ?? [] - const groupIndex = groups.findIndex((g) => g.id === data.groupId) - if (groupIndex === -1) { - throw new Error(`Workflow group "${data.groupId}" not found`) - } - const group = groups[groupIndex] - - // Normalize every caller-supplied column reference to its stable id, so - // the diff/splice/clear logic below operates uniformly in id-space (the - // row-data storage key). New output columns get ids first; then output - // `columnName`, deps, input mappings, and mapping-update targets are - // remapped name → id. Callers that already pass ids are unaffected. - const newColDefs = (data.newOutputColumns ?? []).map((col) => - col.id ? col : { ...col, id: generateColumnId() } - ) - const idByName = new Map( - [...schema.columns, ...newColDefs].map((c) => [c.name, getColumnId(c)]) - ) - const remapRef = (ref: string) => idByName.get(ref) ?? ref - const outputsInput = data.outputs?.map((o) => ({ ...o, columnName: remapRef(o.columnName) })) - const dependenciesInput = data.dependencies - ? { columns: data.dependencies.columns?.map(remapRef) } - : undefined - const inputMappingsInput = data.inputMappings?.map((m) => ({ - ...m, - columnName: remapRef(m.columnName), - })) - const mappingUpdatesNorm = mappingUpdates.map((u) => ({ - ...u, - columnName: remapRef(u.columnName), - })) - // Re-key the out-of-lock leaf-type resolution to ids to match. - const remapLeafTypeById = new Map() - for (const [name, type] of remapLeafTypeByColumn) remapLeafTypeById.set(remapRef(name), type) - - // Apply `mappingUpdates` first: each entry repoints an existing output's - // `(blockId, path)` while preserving the column. We patch the **old** view - // of outputs so the downstream `(blockId, path)`-keyed diff doesn't see the - // swap as a remove+add. The corresponding row data is cleared after the - // schema write so stale values from the old source don't linger. - const remappedColumnIds = new Set() - // Per-column type override (keyed by id) resolved (out-of-lock) from the - // new mapping's leaf type. Only populated when a remap actually changes - // the column's type against the fresh schema. - const remappedColumnTypes = new Map() - let oldOutputs = group.outputs - if (mappingUpdatesNorm.length > 0) { - const updateById = new Map(mappingUpdatesNorm.map((u) => [u.columnName, u])) - for (const u of mappingUpdatesNorm) { - const exists = oldOutputs.some((o) => o.columnName === u.columnName) - if (!exists) { - throw new Error( - `Mapping update for unknown column "${u.columnName}" (group ${data.groupId}).` - ) - } - } - oldOutputs = oldOutputs.map((o) => { - const u = updateById.get(o.columnName) - if (!u) return o - remappedColumnIds.add(o.columnName) - return { ...o, blockId: u.blockId, path: u.path } - }) - - // Only apply the out-of-lock leaf-type resolution if the group still - // points at the workflow we resolved against. If a concurrent writer - // changed `workflowId` between phase 1 and now, those types are stale — - // leave column types unchanged (best-effort, same as a resolution - // failure) rather than stamping types from the old workflow. - const finalWorkflowId = data.workflowId ?? group.workflowId - if (remapLeafTypeById.size > 0 && resolvedForWorkflowId !== finalWorkflowId) { - logger.warn( - `[${requestId}] Workflow group "${data.groupId}" workflowId changed between leaf-type resolution and apply; leaving remapped column types unchanged.` - ) - } else { - const colById = new Map(schema.columns.map((c) => [getColumnId(c), c])) - for (const u of mappingUpdatesNorm) { - const newType = remapLeafTypeById.get(u.columnName) - if (!newType) continue - const oldType = colById.get(u.columnName)?.type - if (newType !== oldType) { - remappedColumnTypes.set(u.columnName, newType) - } - } - } - } - - // If the caller passed `outputs`, that's the new full set. If only - // `mappingUpdates` was sent, the new set is the remapped old set. - const newOutputs = outputsInput ?? oldOutputs - // Enrichment outputs all share empty `blockId`/`path`, so keying on those - // alone collapses every sibling to one entry (dropping columns on diff). Key - // on the registry `outputId` when present; fall back to `blockId::path` for - // workflow outputs. - const oldKey = (o: WorkflowGroupOutput) => - o.outputId ? `out::${o.outputId}` : `${o.blockId}::${o.path}` - const oldByKey = new Map(oldOutputs.map((o) => [oldKey(o), o])) - const newByKey = new Map(newOutputs.map((o) => [oldKey(o), o])) - - const removed = oldOutputs.filter((o) => !newByKey.has(oldKey(o))) - const added = newOutputs.filter((o) => !oldByKey.has(oldKey(o))) - const newColById = new Map(newColDefs.map((c) => [getColumnId(c), c])) - - for (const out of added) { - if (!newColById.has(out.columnName)) { - throw new Error( - `Missing column definition for new output "${out.columnName}" (group ${data.groupId}).` - ) - } - } - - const removedColumnIds = new Set(removed.map((o) => o.columnName)) - let nextColumns = schema.columns - .filter((c) => !removedColumnIds.has(getColumnId(c))) - .map((c) => { - const newType = remappedColumnTypes.get(getColumnId(c)) - return newType ? { ...c, type: newType } : c - }) - if (newColDefs.length > 0) { - // Splice the new column defs into the group's contiguous run rather than - // appending at the end. The desired in-group order is `newOutputs` (the - // sidebar's BFS-of-the-workflow ordering); we walk it, anchor at the first - // surviving sibling's index in `nextColumns`, and emit each output's - // column def in turn. - const groupColIds = new Set(newOutputs.map((o) => o.columnName)) - const firstGroupIdx = nextColumns.findIndex((c) => groupColIds.has(getColumnId(c))) - const anchorIdx = firstGroupIdx === -1 ? nextColumns.length : firstGroupIdx - const orderedGroupCols: ColumnDefinition[] = [] - for (const out of newOutputs) { - const fresh = newColById.get(out.columnName) - if (fresh) { - orderedGroupCols.push(fresh) - } else { - const existing = nextColumns.find((c) => getColumnId(c) === out.columnName) - if (existing) orderedGroupCols.push(existing) - } - } - const remaining = nextColumns.filter((c) => !groupColIds.has(getColumnId(c))) - nextColumns = [ - ...remaining.slice(0, anchorIdx), - ...orderedGroupCols, - ...remaining.slice(anchorIdx), - ] - } - - const updatedGroup: WorkflowGroup = { - ...group, - workflowId: data.workflowId ?? group.workflowId, - name: data.name ?? group.name, - dependencies: dependenciesInput ?? group.dependencies, - outputs: newOutputs, - ...(inputMappingsInput !== undefined ? { inputMappings: inputMappingsInput } : {}), - ...(data.deploymentMode !== undefined ? { deploymentMode: data.deploymentMode } : {}), - ...(data.type !== undefined ? { type: data.type } : {}), - ...(data.autoRun !== undefined ? { autoRun: data.autoRun } : {}), - } - // Removed outputs may be referenced as deps by sibling groups; strip those - // refs so we don't leave dangling-column deps that fail schema validation. - const nextGroups = groups - .map((g, i) => (i === groupIndex ? updatedGroup : g)) - .map((g) => (g.id === updatedGroup.id ? g : stripGroupDeps(g, removedColumnIds))) - const updatedSchema: TableSchema = { - ...schema, - columns: nextColumns, - workflowGroups: nextGroups, - } - - // `columnOrder` (column ids) mirrors the schema layout. Drop removed - // columns, then splice the new ones in at the same anchor as `nextColumns` - // so the table renders them inside the group's contiguous run. - let updatedColumnOrder = table.metadata?.columnOrder?.filter( - (id) => !removedColumnIds.has(id) - ) - if (updatedColumnOrder && newColDefs.length > 0) { - const newColIds = new Set(newColDefs.map(getColumnId)) - const orderWithoutNew = updatedColumnOrder.filter((id) => !newColIds.has(id)) - const groupColIds = new Set(newOutputs.map((o) => o.columnName)) - const orderedGroupIds = newOutputs.map((o) => o.columnName) - const firstGroupOrderIdx = orderWithoutNew.findIndex((id) => groupColIds.has(id)) - const anchorOrderIdx = - firstGroupOrderIdx === -1 ? orderWithoutNew.length : firstGroupOrderIdx - const remainingOrder = orderWithoutNew.filter((id) => !groupColIds.has(id)) - updatedColumnOrder = [ - ...remainingOrder.slice(0, anchorOrderIdx), - ...orderedGroupIds, - ...remainingOrder.slice(anchorOrderIdx), - ] - } - assertValidSchema(updatedSchema, updatedColumnOrder) - - const updatedMetadata: TableMetadata | null = - updatedColumnOrder && table.metadata - ? { ...table.metadata, columnOrder: updatedColumnOrder } - : table.metadata - ? { ...table.metadata } - : null - - const now = new Date() - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - for (const id of removedColumnIds) { - await trx.execute( - sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` - ) - } - // Remapped columns: clear stale values in-tx so rows the backfill can't - // repopulate (no log, no matching span output) end up empty rather than - // retaining the previous mapping's value. The backfill below then writes - // the new mapping's value into rows where it can find one. - for (const id of remappedColumnIds) { - if (removedColumnIds.has(id)) continue - await trx.execute( - sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` - ) - } - - logger.info( - `[${requestId}] Updated workflow group "${data.groupId}" in table ${data.tableId} (added=${added.length}, removed=${removed.length}, remapped=${remappedColumnIds.size})` - ) - - const updatedTable: TableDefinition = { - ...table, - schema: updatedSchema, - metadata: updatedMetadata, - updatedAt: now, - } - return { - updatedTable, - added, - remappedColumnIds, - newOutputs, - previousAutoRun: group.autoRun, - } - }) - - // Backfill from saved execution logs so already-completed group runs surface - // the schema changes without re-running the workflow. Two passes: - // - added outputs (new columns): never overwrite hand-edited values. - // - remapped outputs (existing column re-pointed): overwrite, since the - // new mapping is the source of truth and the user expects the cell to - // refresh to the new output's value. - // Small tables backfill inline-awaited (response returns with consistent - // data); large ones run as a background job. A failed backfill is logged - // but doesn't fail the request — the schema change has already committed. - // Lazy import: backfill-runner closes a cycle back to this module. - const { maybeBackfillGroupOutputs } = await import('./backfill-runner') - if (added.length > 0) { - try { - await maybeBackfillGroupOutputs({ - table: updatedTable, - groupId: data.groupId, - outputs: added, - overwrite: false, - requestId, - actorUserId: data.actorUserId, - }) - } catch (err) { - logger.warn( - `[${requestId}] Backfill from execution logs failed for ${data.tableId} group ${data.groupId}:`, - err - ) - } - } - if (remappedColumnIds.size > 0) { - const remappedOutputs = newOutputs.filter((o) => remappedColumnIds.has(o.columnName)) - try { - await maybeBackfillGroupOutputs({ - table: updatedTable, - groupId: data.groupId, - outputs: remappedOutputs, - overwrite: true, - requestId, - actorUserId: data.actorUserId, - }) - } catch (err) { - logger.warn( - `[${requestId}] Remap backfill from execution logs failed for ${data.tableId} group ${data.groupId}:`, - err - ) - } - } - - // autoRun toggled false → true: fire deps-satisfied rows now via the - // dispatcher. Mirrors the post-add path so re-enabling auto-fire doesn't - // require manual run clicks for rows that are already eligible. - if (previousAutoRun === false && data.autoRun === true) { - void runWorkflowColumn({ - tableId: updatedTable.id, - workspaceId: updatedTable.workspaceId, - mode: 'new', - isManualRun: false, - groupIds: [data.groupId], - requestId, - triggeredByUserId: data.actorUserId, - }).catch((err) => - logger.error(`[${requestId}] auto-dispatch (updateWorkflowGroup autoRun=true) failed:`, err) - ) - } - - return updatedTable -} - -/** - * Adds a single output to an existing workflow group. Mirrors `addTableColumn` - * for plain columns: one canonical op, one column created, type inferred from - * the workflow's flattened outputs (`leafType` for `(blockId, path)`). The - * column is spliced into the group's contiguous run so the table renders the - * new output next to its siblings. - */ -export async function addWorkflowGroupOutput( - data: { - tableId: string - groupId: string - blockId: string - path: string - /** Optional override; defaults to a slug derived from `path`. */ - columnName?: string - /** The member adding the output — billed/gated for any backfill-triggered re-run. */ - actorUserId?: string | null - }, - requestId: string -): Promise { - // Phase 1 (no lock): load the workflow and resolve the pickable output plus - // its execution-order index. This depends only on the workflow graph (which - // is stable), so it runs OFF the advisory-lock critical section — holding the - // lock during this DB load would make concurrent adders on the same table - // time out waiting (the Mothership fan-out this fix targets). Phase 2 - // re-validates that the group still maps to the same workflow under the lock. - const preTable = await getTableById(data.tableId) - if (!preTable) throw new Error('Table not found') - const preGroup = (preTable.schema.workflowGroups ?? []).find((g) => g.id === data.groupId) - if (!preGroup) { - throw new Error(`Workflow group "${data.groupId}" not found`) - } - const workflowId = preGroup.workflowId - - const [ - { loadWorkflowFromNormalizedTables }, - { flattenWorkflowOutputs, getBlockExecutionOrder }, - { columnTypeForLeaf, deriveOutputColumnName }, - ] = await Promise.all([ - import('@/lib/workflows/persistence/utils'), - import('@/lib/workflows/blocks/flatten-outputs'), - import('./column-naming'), - ]) - const normalized = await loadWorkflowFromNormalizedTables(workflowId) - if (!normalized) { - throw new Error(`Workflow ${workflowId} not found`) - } - const blocks = Object.values(normalized.blocks ?? {}).map((b) => ({ - id: b.id, - type: b.type, - name: b.name, - triggerMode: (b as { triggerMode?: boolean }).triggerMode, - subBlocks: b.subBlocks as Record | undefined, - })) - const flattened = flattenWorkflowOutputs(blocks, normalized.edges ?? []) - const match = flattened.find((f) => f.blockId === data.blockId && f.path === data.path) - if (!match) { - throw new Error( - `Output ${data.blockId}::${data.path} is not a valid pickable output on workflow ${workflowId}` - ) - } - const newColumnType = columnTypeForLeaf(match.leafType) - const distances = getBlockExecutionOrder(blocks, normalized.edges ?? []) - const flatIndex = new Map(flattened.map((f, i) => [`${f.blockId}::${f.path}`, i])) - - // Phase 2 (locked): re-read fresh, validate against the current schema, and - // write. The critical section holds no I/O — just the in-memory splice + the - // schema UPDATE — so concurrent adders queue behind it quickly. - const { updatedTable, newOutput } = await withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const groups = schema.workflowGroups ?? [] - const groupIndex = groups.findIndex((g) => g.id === data.groupId) - if (groupIndex === -1) { - throw new Error(`Workflow group "${data.groupId}" not found`) - } - const group = groups[groupIndex] - if (group.workflowId !== workflowId) { - throw new Error( - `Workflow group "${data.groupId}" was remapped to a different workflow concurrently; retry the add.` - ) - } - - if (group.outputs.some((o) => o.blockId === data.blockId && o.path === data.path)) { - throw new Error( - `Workflow group "${data.groupId}" already has an output at ${data.blockId}::${data.path}` - ) - } - - const taken = new Set(schema.columns.map((c) => c.name)) - const columnName = data.columnName ?? deriveOutputColumnName(data.path, taken) - if (!NAME_PATTERN.test(columnName)) { - throw new Error(`Invalid column name "${columnName}". Must satisfy ${NAME_PATTERN.source}.`) - } - if (taken.has(columnName)) { - throw new Error(`Column "${columnName}" already exists`) - } - if (schema.columns.length + 1 > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { - throw new Error( - `Adding a column would exceed the maximum (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE}).` - ) - } - - const newColDef: ColumnDefinition = { - id: generateColumnId(), - name: columnName, - type: newColumnType, - required: false, - unique: false, - workflowGroupId: data.groupId, - } - const newColumnId = getColumnId(newColDef) - const newOutput: WorkflowGroupOutput = { - blockId: data.blockId, - path: data.path, - columnName: newColumnId, - } - - // Sort all of the group's outputs (existing + new) in workflow execution - // order: BFS distance from the start block ASC, with discovery order as - // tiebreak. This matches what the column-sidebar does at create time, so - // columns from the same workflow always read in the order their blocks run - // — regardless of whether they were added at create time or one-by-one. - const groupColIdsBefore = new Set(group.outputs.map((o) => o.columnName)) - const orderKey = (o: { blockId: string; path: string }) => { - const d = distances[o.blockId] - const dist = d === undefined || d < 0 ? Number.POSITIVE_INFINITY : d - const idx = flatIndex.get(`${o.blockId}::${o.path}`) ?? Number.POSITIVE_INFINITY - return [dist, idx] as const - } - const allGroupOutputs = [...group.outputs, newOutput].sort((a, b) => { - const [da, ia] = orderKey(a) - const [db, ib] = orderKey(b) - return da !== db ? da - db : ia - ib - }) - const orderedGroupColIds = allGroupOutputs.map((o) => o.columnName) - const updatedGroup: WorkflowGroup = { - ...group, - outputs: allGroupOutputs, - } - const nextGroups = groups.map((g, i) => (i === groupIndex ? updatedGroup : g)) - - // Splice the new column run into nextColumns: keep the columns outside the - // group where they were, replace the group's contiguous run with the - // BFS-ordered list. Anchor at the position of the first existing sibling - // (or append if the group was empty). - const colById = new Map(schema.columns.map((c) => [getColumnId(c), c])) - const orderedGroupCols: ColumnDefinition[] = orderedGroupColIds.map((id) => { - if (id === newColumnId) return newColDef - const existing = colById.get(id) - if (!existing) { - throw new Error(`Internal: column "${id}" missing while splicing group outputs`) - } - return existing - }) - const remainingCols = schema.columns.filter((c) => !groupColIdsBefore.has(getColumnId(c))) - const firstGroupIdx = schema.columns.findIndex((c) => groupColIdsBefore.has(getColumnId(c))) - const colAnchor = firstGroupIdx === -1 ? remainingCols.length : firstGroupIdx - const nextColumns = [ - ...remainingCols.slice(0, colAnchor), - ...orderedGroupCols, - ...remainingCols.slice(colAnchor), - ] - - const updatedSchema: TableSchema = { - ...schema, - columns: nextColumns, - workflowGroups: nextGroups, - } - - const updatedColumnOrder = table.metadata?.columnOrder - ? (() => { - const orderWithoutGroup = table.metadata!.columnOrder!.filter( - (id) => !groupColIdsBefore.has(id) - ) - const firstGroupOrderIdx = table.metadata!.columnOrder!.findIndex((id) => - groupColIdsBefore.has(id) - ) - const orderAnchor = - firstGroupOrderIdx === -1 ? orderWithoutGroup.length : firstGroupOrderIdx - return [ - ...orderWithoutGroup.slice(0, orderAnchor), - ...orderedGroupColIds, - ...orderWithoutGroup.slice(orderAnchor), - ] - })() - : undefined - - assertValidSchema(updatedSchema, updatedColumnOrder) - - const updatedMetadata: TableMetadata | null = - updatedColumnOrder && table.metadata - ? { ...table.metadata, columnOrder: updatedColumnOrder } - : table.metadata - ? { ...table.metadata } - : null - - const now = new Date() - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - - logger.info( - `[${requestId}] Added output "${columnName}" (${newColDef.type}) to workflow group "${data.groupId}" in table ${data.tableId}` - ) - - const updatedTable: TableDefinition = { - ...table, - schema: updatedSchema, - metadata: updatedMetadata, - updatedAt: now, - } - return { updatedTable, newOutput } - }) - - // Backfill from saved execution logs — same flow `updateWorkflowGroup` - // uses for added outputs. Reads each row's saved trace spans for the - // group's executionId and writes the new output's value back. Existing - // rows that have hand-edited values are left alone (overwrite: false). - // Cheap compared to re-running the workflow on every row, which is what - // an earlier version of this code did — that mistakenly fanned out N - // workflow-group-cell jobs and burned compute the user didn't ask for. - // Small tables backfill inline; large ones run as a background job. - // Lazy import: backfill-runner closes a cycle back to this module. - try { - const { maybeBackfillGroupOutputs } = await import('./backfill-runner') - await maybeBackfillGroupOutputs({ - table: updatedTable, - groupId: data.groupId, - outputs: [newOutput], - overwrite: false, - requestId, - actorUserId: data.actorUserId, - }) - } catch (err) { - logger.warn( - `[${requestId}] Backfill from execution logs failed for ${data.tableId} group ${data.groupId} after adding output "${newOutput.columnName}":`, - err - ) - } - - return updatedTable -} - -/** - * Removes a single output from a workflow group. Drops the bound column and - * strips the value from every row's `data` JSONB. If the output is the - * group's last, the empty group is left in place — drop it explicitly with - * `deleteWorkflowGroup` if needed. - */ -export async function deleteWorkflowGroupOutput( - data: { tableId: string; groupId: string; columnName: string }, - requestId: string -): Promise { - return withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const groups = schema.workflowGroups ?? [] - const groupIndex = groups.findIndex((g) => g.id === data.groupId) - if (groupIndex === -1) { - throw new Error(`Workflow group "${data.groupId}" not found`) - } - const group = groups[groupIndex] - // `data.columnName` may be a column id (first-party) or display name - // (mothership/legacy); resolve to the stable id used everywhere below. - const targetColumn = schema.columns.find((c) => columnMatchesRef(c, data.columnName)) - const columnId = targetColumn ? getColumnId(targetColumn) : data.columnName - if (!group.outputs.some((o) => o.columnName === columnId)) { - throw new Error( - `Workflow group "${data.groupId}" has no output bound to column "${data.columnName}"` - ) - } - - const updatedGroup: WorkflowGroup = { - ...group, - outputs: group.outputs.filter((o) => o.columnName !== columnId), - } - const nextGroups = groups.map((g, i) => (i === groupIndex ? updatedGroup : g)) - const nextColumns = schema.columns.filter((c) => getColumnId(c) !== columnId) - const updatedSchema: TableSchema = { - ...schema, - columns: nextColumns, - workflowGroups: nextGroups, - } - - const updatedColumnOrder = table.metadata?.columnOrder?.filter((id) => id !== columnId) - assertValidSchema(updatedSchema, updatedColumnOrder) - - const updatedMetadata: TableMetadata | null = - updatedColumnOrder && table.metadata - ? { ...table.metadata, columnOrder: updatedColumnOrder } - : table.metadata - ? { ...table.metadata } - : null - - const now = new Date() - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - await trx.execute( - sql`UPDATE user_table_rows SET data = data - ${columnId}::text WHERE table_id = ${data.tableId} AND data ? ${columnId}::text` - ) - - logger.info( - `[${requestId}] Removed output "${data.columnName}" from workflow group "${data.groupId}" in table ${data.tableId}` - ) - - return { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now } - }) -} - -/** - * Removes a workflow group plus all its output columns. Also strips the - * group's `executions[groupId]` entry from every row. - */ -export async function deleteWorkflowGroup( - data: DeleteWorkflowGroupData, - requestId: string -): Promise { - return withLockedTable(data.tableId, async (table, trx) => { - const schema = table.schema - const groups = schema.workflowGroups ?? [] - const group = groups.find((g) => g.id === data.groupId) - if (!group) { - throw new Error(`Workflow group "${data.groupId}" not found`) - } - - const removedColumnIds = new Set(group.outputs.map((o) => o.columnName)) - // Removed group's output columns may be referenced as deps by sibling groups. - // Strip those refs so we don't leave dangling-column deps behind. - const nextGroups = groups - .filter((g) => g.id !== data.groupId) - .map((g) => stripGroupDeps(g, removedColumnIds)) - const updatedSchema: TableSchema = { - ...schema, - columns: schema.columns.filter((c) => !removedColumnIds.has(getColumnId(c))), - workflowGroups: nextGroups, - } - const updatedColumnOrder = table.metadata?.columnOrder?.filter( - (id) => !removedColumnIds.has(id) - ) - assertValidSchema(updatedSchema, updatedColumnOrder) - - const updatedMetadata: TableMetadata | null = - updatedColumnOrder && table.metadata - ? { ...table.metadata, columnOrder: updatedColumnOrder } - : table.metadata - ? { ...table.metadata } - : null - - const now = new Date() - await setTableTxTimeouts(trx, { statementMs: 60_000 }) - await trx - .update(userTableDefinitions) - .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) - .where(eq(userTableDefinitions.id, data.tableId)) - for (const id of removedColumnIds) { - await trx.execute( - sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` - ) - } - await stripGroupExecutions(trx, data.tableId, [data.groupId]) - - logger.info( - `[${requestId}] Deleted workflow group "${data.groupId}" from table ${data.tableId}` - ) - - return { - ...table, - schema: updatedSchema, - metadata: updatedMetadata, - updatedAt: now, - } - }) -} - -/** - * Checks if a value is compatible with a target column type. - */ -function isValueCompatibleWithType( - value: unknown, - targetType: (typeof COLUMN_TYPES)[number] -): boolean { - if (value === null || value === undefined) return true - - switch (targetType) { - case 'string': - return true - case 'number': { - if (typeof value === 'number') return Number.isFinite(value) - if (typeof value === 'string') { - const num = Number(value) - return Number.isFinite(num) && value.trim() !== '' - } - return false - } - case 'boolean': { - if (typeof value === 'boolean') return true - if (typeof value === 'string') - return ['true', 'false', '1', '0'].includes(value.toLowerCase()) - if (typeof value === 'number') return value === 0 || value === 1 - return false - } - case 'date': { - if (value instanceof Date) return !Number.isNaN(value.getTime()) - if (typeof value === 'string') return !Number.isNaN(Date.parse(value)) - return false - } - case 'json': - return true - default: - return false - } -} diff --git a/apps/sim/lib/table/sql.ts b/apps/sim/lib/table/sql.ts index 7e064468a26..1e4f4a35ea0 100644 --- a/apps/sim/lib/table/sql.ts +++ b/apps/sim/lib/table/sql.ts @@ -7,9 +7,15 @@ import type { SQL } from 'drizzle-orm' import { sql } from 'drizzle-orm' -import { getColumnId } from './column-keys' -import { NAME_PATTERN } from './constants' -import type { ColumnDefinition, ConditionOperators, Filter, JsonValue, Sort } from './types' +import { getColumnId } from '@/lib/table/column-keys' +import { NAME_PATTERN } from '@/lib/table/constants' +import type { + ColumnDefinition, + ConditionOperators, + Filter, + JsonValue, + Sort, +} from '@/lib/table/types' /** * Error thrown when caller-supplied filter or sort input is malformed. diff --git a/apps/sim/lib/table/tx.ts b/apps/sim/lib/table/tx.ts new file mode 100644 index 00000000000..4e0b8b2dcbe --- /dev/null +++ b/apps/sim/lib/table/tx.ts @@ -0,0 +1,50 @@ +/** + * Shared transaction / locking helpers for the table service layer. + * + * Internal module: not exposed via the `@/lib/table` barrel. Consumers import + * directly from `@/lib/table/tx`. + */ + +import { sql } from 'drizzle-orm' +import type { DbTransaction } from '@/lib/table/planner' + +const TIMEOUT_CAP_MS = 10 * 60_000 + +/** + * Sets per-transaction Postgres timeouts via `SET LOCAL`. + * + * `lock_timeout` is the critical one: without it, a waiter inherits the full + * `statement_timeout` clock, so one stuck writer can drain the pool. + * + * Safe under pgBouncer transaction pooling — `SET LOCAL` is transaction-scoped + * and cleared at COMMIT/ROLLBACK before the session returns to the pool. + */ +export async function setTableTxTimeouts( + trx: DbTransaction, + opts?: { statementMs?: number; lockMs?: number; idleMs?: number } +) { + const s = opts?.statementMs ?? 10_000 + const l = opts?.lockMs ?? 3_000 + const i = opts?.idleMs ?? 5_000 + await trx.execute(sql.raw(`SET LOCAL statement_timeout = '${s}ms'`)) + await trx.execute(sql.raw(`SET LOCAL lock_timeout = '${l}ms'`)) + await trx.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = '${i}ms'`)) +} + +/** + * Scales `statement_timeout` to the expected row-count work. + * + * Bulk operations that rewrite JSONB or cascade row triggers (e.g. + * `replaceTableRows`, `deleteColumn`, `renameColumn`) scale roughly linearly + * with row count. A fixed cap would regress large-table users who never saw a + * timeout before `SET LOCAL` was introduced. This helper picks + * `max(baseMs, rowCount * perRowMs)`, capped at 10 minutes so a single + * runaway transaction cannot indefinitely pin a pool connection. + */ +export function scaledStatementTimeoutMs( + rowCount: number, + opts: { baseMs: number; perRowMs: number } +): number { + const safeRowCount = Math.max(0, rowCount) + return Math.min(TIMEOUT_CAP_MS, Math.max(opts.baseMs, safeRowCount * opts.perRowMs)) +} diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index aaa0760a3e3..cf57842617a 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -2,7 +2,7 @@ * Type definitions for user-defined tables. */ -import type { COLUMN_TYPES } from './constants' +import type { COLUMN_TYPES } from '@/lib/table/constants' export type ColumnValue = string | number | boolean | null | Date export type JsonValue = ColumnValue | JsonValue[] | { [key: string]: JsonValue } diff --git a/apps/sim/lib/table/validation.ts b/apps/sim/lib/table/validation.ts index fa3aacaf98b..b42e2a62b8d 100644 --- a/apps/sim/lib/table/validation.ts +++ b/apps/sim/lib/table/validation.ts @@ -6,10 +6,16 @@ import { db } from '@sim/db' import { userTableRows } from '@sim/db/schema' import { and, eq, or, type SQL, sql } from 'drizzle-orm' import { NextResponse } from 'next/server' -import { getColumnId } from './column-keys' -import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from './constants' -import { withSeqscanOff } from './planner' -import type { ColumnDefinition, JsonValue, RowData, TableSchema, ValidationResult } from './types' +import { getColumnId } from '@/lib/table/column-keys' +import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' +import { withSeqscanOff } from '@/lib/table/planner' +import type { + ColumnDefinition, + JsonValue, + RowData, + TableSchema, + ValidationResult, +} from '@/lib/table/types' export type { ColumnDefinition, TableSchema, ValidationResult } diff --git a/apps/sim/lib/table/workflow-columns.ts b/apps/sim/lib/table/workflow-columns.ts index 8adbe32a5eb..231f020d44b 100644 --- a/apps/sim/lib/table/workflow-columns.ts +++ b/apps/sim/lib/table/workflow-columns.ts @@ -31,16 +31,16 @@ import type { const logger = createLogger('WorkflowGroupScheduler') -import { getColumnId } from './column-keys' -import { USER_TABLE_ROWS_SQL_NAME } from './constants' -import { areGroupDepsSatisfied, areOutputsFilled, isExecInFlight } from './deps' -import type { DispatchLimit, DispatchMode } from './dispatcher' -import { buildFilterClause } from './sql' +import { getColumnId } from '@/lib/table/column-keys' +import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' +import { areGroupDepsSatisfied, areOutputsFilled, isExecInFlight } from '@/lib/table/deps' +import type { DispatchLimit, DispatchMode } from '@/lib/table/dispatcher' +import { buildFilterClause } from '@/lib/table/sql' export { getUnmetGroupDeps, optimisticallyScheduleNewlyEligibleGroups, -} from './deps' +} from '@/lib/table/deps' /** * Per-(row, group) eligibility for both the auto-fire reactor and manual @@ -367,9 +367,12 @@ export async function cancelWorkflowGroupRuns( rowId?: string, options?: { groupIds?: string[]; filter?: Filter; excludeRowIds?: string[] } ): Promise { - const { getTableById, updateRow } = await import('@/lib/table/service') + const { getTableById } = await import('@/lib/table/service') + const { updateRow } = await import('@/lib/table/rows/service') const { getJobQueue } = await import('@/lib/core/async-jobs/config') - const { listActiveDispatches, markActiveDispatchesCancelled } = await import('./dispatcher') + const { listActiveDispatches, markActiveDispatchesCancelled } = await import( + '@/lib/table/dispatcher' + ) const table = await getTableById(tableId) if (!table) { @@ -661,7 +664,7 @@ export async function runWorkflowColumn(opts: { if (rowIds && rowIds.length === 0) return { dispatchId: null } // Lazy imports: `./service` and `./dispatcher` both close cycles back to // this module; `@trigger.dev/sdk` is heavy and only needed on this op. - const { getTableById } = await import('./service') + const { getTableById } = await import('@/lib/table/service') const table = await getTableById(tableId) if (!table) throw new Error('Table not found') if (table.workspaceId !== workspaceId) throw new Error('Invalid workspace ID') diff --git a/apps/sim/lib/table/workflow-groups/service.ts b/apps/sim/lib/table/workflow-groups/service.ts new file mode 100644 index 00000000000..8479118650d --- /dev/null +++ b/apps/sim/lib/table/workflow-groups/service.ts @@ -0,0 +1,964 @@ +/** + * Workflow-group operations on user tables. + * + * Extracted from the table service: add/update/delete workflow groups and their + * output columns, plus stale-output pruning after a workflow deploy. These ops + * mutate `schema.workflowGroups` (and the bound output columns + row data) under + * the per-table advisory lock from `withLockedTable`. + */ + +import { db } from '@sim/db' +import { userTableDefinitions } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, isNull, sql } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import { + columnMatchesRef, + generateColumnId, + getColumnId, + remapGroupColumnRefs, +} from '@/lib/table/column-keys' +import { NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' +import { stripGroupExecutions } from '@/lib/table/rows/executions' +import { getTableById, withLockedTable } from '@/lib/table/service' +import { setTableTxTimeouts } from '@/lib/table/tx' +import type { + AddWorkflowGroupData, + ColumnDefinition, + DeleteWorkflowGroupData, + TableDefinition, + TableMetadata, + TableSchema, + UpdateWorkflowGroupData, + WorkflowGroup, + WorkflowGroupOutput, +} from '@/lib/table/types' +import { assertValidSchema, runWorkflowColumn, stripGroupDeps } from '@/lib/table/workflow-columns' + +const logger = createLogger('TableWorkflowGroupsService') +/** + * Drops references to deleted blocks from every workflow group on every table + * that targets the just-deployed workflow. Called from the workflow deploy + * orchestrator after the new deployment commits, so the table UI never holds + * stale `{blockId, path}` entries for blocks the user removed. + * + * - Filters `outputs[]` per group. If every output would be filtered out, the + * group is left untouched and a warning is logged — the user must + * reconfigure it manually. + * - Scoped to the workflow's workspace. + * - Idempotent: running twice with the same `validBlockIds` is a no-op on the + * second pass. Existing row data is left alone. + */ +export async function pruneStaleWorkflowGroupOutputs({ + workflowId, + workspaceId, + validBlockIds, + requestId, + tx, +}: { + workflowId: string + workspaceId: string + validBlockIds: Set + requestId: string + tx?: DbOrTx +}): Promise { + const executor = tx ?? db + const tables = await executor + .select({ + id: userTableDefinitions.id, + schema: userTableDefinitions.schema, + }) + .from(userTableDefinitions) + .where( + and( + eq(userTableDefinitions.workspaceId, workspaceId), + isNull(userTableDefinitions.archivedAt) + ) + ) + + for (const t of tables) { + const schema = t.schema as TableSchema + const groups = schema.workflowGroups ?? [] + if (groups.length === 0) continue + + let mutated = false + const nextGroups = groups.map((group) => { + if (group.workflowId !== workflowId) return group + const filtered = group.outputs.filter((o) => validBlockIds.has(o.blockId)) + if (filtered.length === group.outputs.length) return group + if (filtered.length === 0) { + logger.warn( + `[${requestId}] All outputs for workflow group "${group.name ?? group.id}" in table ${t.id} reference deleted blocks; leaving group intact for user reconfiguration.` + ) + return group + } + mutated = true + return { ...group, outputs: filtered } + }) + + if (!mutated) continue + + await executor + .update(userTableDefinitions) + .set({ + schema: { ...schema, workflowGroups: nextGroups }, + updatedAt: new Date(), + }) + .where(eq(userTableDefinitions.id, t.id)) + + logger.info(`[${requestId}] Pruned stale workflow=${workflowId} block refs from table ${t.id}`) + } +} + +/** + * Atomically inserts a workflow group plus its output columns into a table's + * schema. Both arrays update in one DB write so the schema is never observed + * mid-mutation (e.g. columns referencing a group that doesn't yet exist). + */ +export async function addWorkflowGroup( + data: AddWorkflowGroupData, + requestId: string +): Promise { + const updatedTable = await withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const groups = schema.workflowGroups ?? [] + if (groups.some((g) => g.id === data.group.id)) { + throw new Error(`Workflow group "${data.group.id}" already exists`) + } + + const existingNames = new Set(schema.columns.map((c) => c.name.toLowerCase())) + for (const col of data.outputColumns) { + if (!NAME_PATTERN.test(col.name)) { + throw new Error( + `Invalid output column name "${col.name}". Must satisfy ${NAME_PATTERN.source}.` + ) + } + if (existingNames.has(col.name.toLowerCase())) { + throw new Error(`Column "${col.name}" already exists`) + } + } + + if (schema.columns.length + data.outputColumns.length > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { + throw new Error( + `Adding ${data.outputColumns.length} columns would exceed the maximum (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE}).` + ) + } + + // Assign stable ids to the new output columns, then rewrite the group's + // column refs from name → id so outputs/deps/inputMappings key on ids — + // matching the row-data storage key and surviving future renames. + const outputColumns = data.outputColumns.map((col) => + col.id ? col : { ...col, id: generateColumnId() } + ) + const updatedColumns = [...schema.columns, ...outputColumns] + const idByName = new Map(updatedColumns.map((c) => [c.name, getColumnId(c)])) + const group = remapGroupColumnRefs(data.group, idByName) + + const updatedSchema: TableSchema = { + ...schema, + columns: updatedColumns, + workflowGroups: [...groups, group], + } + + // Keep `metadata.columnOrder` (column ids) in sync — see `addTableColumn`. + // New output columns get appended in the order the caller supplied. + const existingOrder = table.metadata?.columnOrder + let updatedMetadata = table.metadata + if (existingOrder && existingOrder.length > 0) { + const known = new Set(existingOrder) + const append = outputColumns.map(getColumnId).filter((id) => !known.has(id)) + if (append.length > 0) { + updatedMetadata = { ...table.metadata, columnOrder: [...existingOrder, ...append] } + } + } + + assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) + + const now = new Date() + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + logger.info( + `[${requestId}] Added workflow group "${data.group.id}" with ${data.outputColumns.length} output column(s) to table ${data.tableId}` + ) + + return { + ...table, + schema: updatedSchema, + metadata: updatedMetadata, + updatedAt: now, + } + }) + + // Auto-fire existing rows whose deps are already met for the new group. + // Fire-and-forget — the dispatcher bounds queue depth (window of 20) and + // walks the table in the background. HTTP returns instantly; cells fill + // in over the next minutes as the dispatcher walks. Mothership opts out + // by setting `autoRun: false`. + if (data.autoRun !== false) { + void runWorkflowColumn({ + tableId: updatedTable.id, + workspaceId: updatedTable.workspaceId, + mode: 'new', + isManualRun: false, + groupIds: [data.group.id], + requestId, + triggeredByUserId: data.actorUserId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (addWorkflowGroup) failed:`, err)) + } + + return updatedTable +} + +/** + * Updates a workflow group: any combination of workflowId, name, dependencies, + * outputs[]. Computes added/removed outputs vs current state and inserts / + * removes columns transactionally. Removed outputs also clear their key from + * every row's `data`. + */ +export async function updateWorkflowGroup( + data: UpdateWorkflowGroupData, + requestId: string +): Promise { + const mappingUpdates = data.mappingUpdates ?? [] + + // Phase 1 (no lock): when there are mapping updates, load the workflow once to + // resolve each remap's new leaf type. Kept OFF the advisory-lock critical + // section so concurrent group edits on the same table don't time out waiting + // on this DB load. Best-effort — a resolution failure leaves column types + // unchanged (workflow deleted, block removed). The result is applied against + // the fresh schema under the lock in phase 2. + const remapLeafTypeByColumn = new Map() + // The workflow id the leaf types above were resolved against. Phase 2 only + // applies the resolved types if the group still points at this workflow under + // the lock — a concurrent `workflowId` change would make them stale. + let resolvedForWorkflowId: string | undefined + if (mappingUpdates.length > 0) { + try { + const preTable = await getTableById(data.tableId) + const preGroup = preTable?.schema.workflowGroups?.find((g) => g.id === data.groupId) + const targetWorkflowId = data.workflowId ?? preGroup?.workflowId + if (targetWorkflowId) { + resolvedForWorkflowId = targetWorkflowId + const [ + { loadWorkflowFromNormalizedTables }, + { flattenWorkflowOutputs }, + { columnTypeForLeaf }, + ] = await Promise.all([ + import('@/lib/workflows/persistence/utils'), + import('@/lib/workflows/blocks/flatten-outputs'), + import('@/lib/table/column-naming'), + ]) + const normalized = await loadWorkflowFromNormalizedTables(targetWorkflowId) + if (normalized) { + const blocks = Object.values(normalized.blocks ?? {}).map((b) => ({ + id: b.id, + type: b.type, + name: b.name, + triggerMode: (b as { triggerMode?: boolean }).triggerMode, + subBlocks: b.subBlocks as Record | undefined, + })) + const flattened = flattenWorkflowOutputs(blocks, normalized.edges ?? []) + const flatByKey = new Map(flattened.map((f) => [`${f.blockId}::${f.path}`, f])) + for (const u of mappingUpdates) { + const match = flatByKey.get(`${u.blockId}::${u.path}`) + if (!match) continue + const newType = columnTypeForLeaf(match.leafType) + if (newType) remapLeafTypeByColumn.set(u.columnName, newType) + } + } + } + } catch (err) { + logger.warn( + `[${requestId}] Could not resolve new leaf types for remap on group ${data.groupId}; leaving column types unchanged:`, + err + ) + } + } + + const { updatedTable, added, remappedColumnIds, newOutputs, previousAutoRun } = + await withLockedTable(data.tableId, async (table, trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + + const schema = table.schema + const groups = schema.workflowGroups ?? [] + const groupIndex = groups.findIndex((g) => g.id === data.groupId) + if (groupIndex === -1) { + throw new Error(`Workflow group "${data.groupId}" not found`) + } + const group = groups[groupIndex] + + // Normalize every caller-supplied column reference to its stable id, so + // the diff/splice/clear logic below operates uniformly in id-space (the + // row-data storage key). New output columns get ids first; then output + // `columnName`, deps, input mappings, and mapping-update targets are + // remapped name → id. Callers that already pass ids are unaffected. + const newColDefs = (data.newOutputColumns ?? []).map((col) => + col.id ? col : { ...col, id: generateColumnId() } + ) + const idByName = new Map( + [...schema.columns, ...newColDefs].map((c) => [c.name, getColumnId(c)]) + ) + const remapRef = (ref: string) => idByName.get(ref) ?? ref + const outputsInput = data.outputs?.map((o) => ({ ...o, columnName: remapRef(o.columnName) })) + const dependenciesInput = data.dependencies + ? { columns: data.dependencies.columns?.map(remapRef) } + : undefined + const inputMappingsInput = data.inputMappings?.map((m) => ({ + ...m, + columnName: remapRef(m.columnName), + })) + const mappingUpdatesNorm = mappingUpdates.map((u) => ({ + ...u, + columnName: remapRef(u.columnName), + })) + // Re-key the out-of-lock leaf-type resolution to ids to match. + const remapLeafTypeById = new Map() + for (const [name, type] of remapLeafTypeByColumn) remapLeafTypeById.set(remapRef(name), type) + + // Apply `mappingUpdates` first: each entry repoints an existing output's + // `(blockId, path)` while preserving the column. We patch the **old** view + // of outputs so the downstream `(blockId, path)`-keyed diff doesn't see the + // swap as a remove+add. The corresponding row data is cleared after the + // schema write so stale values from the old source don't linger. + const remappedColumnIds = new Set() + // Per-column type override (keyed by id) resolved (out-of-lock) from the + // new mapping's leaf type. Only populated when a remap actually changes + // the column's type against the fresh schema. + const remappedColumnTypes = new Map() + let oldOutputs = group.outputs + if (mappingUpdatesNorm.length > 0) { + const updateById = new Map(mappingUpdatesNorm.map((u) => [u.columnName, u])) + for (const u of mappingUpdatesNorm) { + const exists = oldOutputs.some((o) => o.columnName === u.columnName) + if (!exists) { + throw new Error( + `Mapping update for unknown column "${u.columnName}" (group ${data.groupId}).` + ) + } + } + oldOutputs = oldOutputs.map((o) => { + const u = updateById.get(o.columnName) + if (!u) return o + remappedColumnIds.add(o.columnName) + return { ...o, blockId: u.blockId, path: u.path } + }) + + // Only apply the out-of-lock leaf-type resolution if the group still + // points at the workflow we resolved against. If a concurrent writer + // changed `workflowId` between phase 1 and now, those types are stale — + // leave column types unchanged (best-effort, same as a resolution + // failure) rather than stamping types from the old workflow. + const finalWorkflowId = data.workflowId ?? group.workflowId + if (remapLeafTypeById.size > 0 && resolvedForWorkflowId !== finalWorkflowId) { + logger.warn( + `[${requestId}] Workflow group "${data.groupId}" workflowId changed between leaf-type resolution and apply; leaving remapped column types unchanged.` + ) + } else { + const colById = new Map(schema.columns.map((c) => [getColumnId(c), c])) + for (const u of mappingUpdatesNorm) { + const newType = remapLeafTypeById.get(u.columnName) + if (!newType) continue + const oldType = colById.get(u.columnName)?.type + if (newType !== oldType) { + remappedColumnTypes.set(u.columnName, newType) + } + } + } + } + + // If the caller passed `outputs`, that's the new full set. If only + // `mappingUpdates` was sent, the new set is the remapped old set. + const newOutputs = outputsInput ?? oldOutputs + // Enrichment outputs all share empty `blockId`/`path`, so keying on those + // alone collapses every sibling to one entry (dropping columns on diff). Key + // on the registry `outputId` when present; fall back to `blockId::path` for + // workflow outputs. + const oldKey = (o: WorkflowGroupOutput) => + o.outputId ? `out::${o.outputId}` : `${o.blockId}::${o.path}` + const oldByKey = new Map(oldOutputs.map((o) => [oldKey(o), o])) + const newByKey = new Map(newOutputs.map((o) => [oldKey(o), o])) + + const removed = oldOutputs.filter((o) => !newByKey.has(oldKey(o))) + const added = newOutputs.filter((o) => !oldByKey.has(oldKey(o))) + const newColById = new Map(newColDefs.map((c) => [getColumnId(c), c])) + + for (const out of added) { + if (!newColById.has(out.columnName)) { + throw new Error( + `Missing column definition for new output "${out.columnName}" (group ${data.groupId}).` + ) + } + } + + const removedColumnIds = new Set(removed.map((o) => o.columnName)) + let nextColumns = schema.columns + .filter((c) => !removedColumnIds.has(getColumnId(c))) + .map((c) => { + const newType = remappedColumnTypes.get(getColumnId(c)) + return newType ? { ...c, type: newType } : c + }) + if (newColDefs.length > 0) { + // Splice the new column defs into the group's contiguous run rather than + // appending at the end. The desired in-group order is `newOutputs` (the + // sidebar's BFS-of-the-workflow ordering); we walk it, anchor at the first + // surviving sibling's index in `nextColumns`, and emit each output's + // column def in turn. + const groupColIds = new Set(newOutputs.map((o) => o.columnName)) + const firstGroupIdx = nextColumns.findIndex((c) => groupColIds.has(getColumnId(c))) + const anchorIdx = firstGroupIdx === -1 ? nextColumns.length : firstGroupIdx + const orderedGroupCols: ColumnDefinition[] = [] + for (const out of newOutputs) { + const fresh = newColById.get(out.columnName) + if (fresh) { + orderedGroupCols.push(fresh) + } else { + const existing = nextColumns.find((c) => getColumnId(c) === out.columnName) + if (existing) orderedGroupCols.push(existing) + } + } + const remaining = nextColumns.filter((c) => !groupColIds.has(getColumnId(c))) + nextColumns = [ + ...remaining.slice(0, anchorIdx), + ...orderedGroupCols, + ...remaining.slice(anchorIdx), + ] + } + + const updatedGroup: WorkflowGroup = { + ...group, + workflowId: data.workflowId ?? group.workflowId, + name: data.name ?? group.name, + dependencies: dependenciesInput ?? group.dependencies, + outputs: newOutputs, + ...(inputMappingsInput !== undefined ? { inputMappings: inputMappingsInput } : {}), + ...(data.deploymentMode !== undefined ? { deploymentMode: data.deploymentMode } : {}), + ...(data.type !== undefined ? { type: data.type } : {}), + ...(data.autoRun !== undefined ? { autoRun: data.autoRun } : {}), + } + // Removed outputs may be referenced as deps by sibling groups; strip those + // refs so we don't leave dangling-column deps that fail schema validation. + const nextGroups = groups + .map((g, i) => (i === groupIndex ? updatedGroup : g)) + .map((g) => (g.id === updatedGroup.id ? g : stripGroupDeps(g, removedColumnIds))) + const updatedSchema: TableSchema = { + ...schema, + columns: nextColumns, + workflowGroups: nextGroups, + } + + // `columnOrder` (column ids) mirrors the schema layout. Drop removed + // columns, then splice the new ones in at the same anchor as `nextColumns` + // so the table renders them inside the group's contiguous run. + let updatedColumnOrder = table.metadata?.columnOrder?.filter( + (id) => !removedColumnIds.has(id) + ) + if (updatedColumnOrder && newColDefs.length > 0) { + const newColIds = new Set(newColDefs.map(getColumnId)) + const orderWithoutNew = updatedColumnOrder.filter((id) => !newColIds.has(id)) + const groupColIds = new Set(newOutputs.map((o) => o.columnName)) + const orderedGroupIds = newOutputs.map((o) => o.columnName) + const firstGroupOrderIdx = orderWithoutNew.findIndex((id) => groupColIds.has(id)) + const anchorOrderIdx = + firstGroupOrderIdx === -1 ? orderWithoutNew.length : firstGroupOrderIdx + const remainingOrder = orderWithoutNew.filter((id) => !groupColIds.has(id)) + updatedColumnOrder = [ + ...remainingOrder.slice(0, anchorOrderIdx), + ...orderedGroupIds, + ...remainingOrder.slice(anchorOrderIdx), + ] + } + assertValidSchema(updatedSchema, updatedColumnOrder) + + const updatedMetadata: TableMetadata | null = + updatedColumnOrder && table.metadata + ? { ...table.metadata, columnOrder: updatedColumnOrder } + : table.metadata + ? { ...table.metadata } + : null + + const now = new Date() + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + for (const id of removedColumnIds) { + await trx.execute( + sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` + ) + } + // Remapped columns: clear stale values in-tx so rows the backfill can't + // repopulate (no log, no matching span output) end up empty rather than + // retaining the previous mapping's value. The backfill below then writes + // the new mapping's value into rows where it can find one. + for (const id of remappedColumnIds) { + if (removedColumnIds.has(id)) continue + await trx.execute( + sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` + ) + } + + logger.info( + `[${requestId}] Updated workflow group "${data.groupId}" in table ${data.tableId} (added=${added.length}, removed=${removed.length}, remapped=${remappedColumnIds.size})` + ) + + const updatedTable: TableDefinition = { + ...table, + schema: updatedSchema, + metadata: updatedMetadata, + updatedAt: now, + } + return { + updatedTable, + added, + remappedColumnIds, + newOutputs, + previousAutoRun: group.autoRun, + } + }) + + // Backfill from saved execution logs so already-completed group runs surface + // the schema changes without re-running the workflow. Two passes: + // - added outputs (new columns): never overwrite hand-edited values. + // - remapped outputs (existing column re-pointed): overwrite, since the + // new mapping is the source of truth and the user expects the cell to + // refresh to the new output's value. + // Small tables backfill inline-awaited (response returns with consistent + // data); large ones run as a background job. A failed backfill is logged + // but doesn't fail the request — the schema change has already committed. + // Lazy import: backfill-runner closes a cycle back to this module. + const { maybeBackfillGroupOutputs } = await import('@/lib/table/backfill-runner') + if (added.length > 0) { + try { + await maybeBackfillGroupOutputs({ + table: updatedTable, + groupId: data.groupId, + outputs: added, + overwrite: false, + requestId, + actorUserId: data.actorUserId, + }) + } catch (err) { + logger.warn( + `[${requestId}] Backfill from execution logs failed for ${data.tableId} group ${data.groupId}:`, + err + ) + } + } + if (remappedColumnIds.size > 0) { + const remappedOutputs = newOutputs.filter((o) => remappedColumnIds.has(o.columnName)) + try { + await maybeBackfillGroupOutputs({ + table: updatedTable, + groupId: data.groupId, + outputs: remappedOutputs, + overwrite: true, + requestId, + actorUserId: data.actorUserId, + }) + } catch (err) { + logger.warn( + `[${requestId}] Remap backfill from execution logs failed for ${data.tableId} group ${data.groupId}:`, + err + ) + } + } + + // autoRun toggled false → true: fire deps-satisfied rows now via the + // dispatcher. Mirrors the post-add path so re-enabling auto-fire doesn't + // require manual run clicks for rows that are already eligible. + if (previousAutoRun === false && data.autoRun === true) { + void runWorkflowColumn({ + tableId: updatedTable.id, + workspaceId: updatedTable.workspaceId, + mode: 'new', + isManualRun: false, + groupIds: [data.groupId], + requestId, + triggeredByUserId: data.actorUserId, + }).catch((err) => + logger.error(`[${requestId}] auto-dispatch (updateWorkflowGroup autoRun=true) failed:`, err) + ) + } + + return updatedTable +} + +/** + * Adds a single output to an existing workflow group. Mirrors `addTableColumn` + * for plain columns: one canonical op, one column created, type inferred from + * the workflow's flattened outputs (`leafType` for `(blockId, path)`). The + * column is spliced into the group's contiguous run so the table renders the + * new output next to its siblings. + */ +export async function addWorkflowGroupOutput( + data: { + tableId: string + groupId: string + blockId: string + path: string + /** Optional override; defaults to a slug derived from `path`. */ + columnName?: string + /** The member adding the output — billed/gated for any backfill-triggered re-run. */ + actorUserId?: string | null + }, + requestId: string +): Promise { + // Phase 1 (no lock): load the workflow and resolve the pickable output plus + // its execution-order index. This depends only on the workflow graph (which + // is stable), so it runs OFF the advisory-lock critical section — holding the + // lock during this DB load would make concurrent adders on the same table + // time out waiting (the Mothership fan-out this fix targets). Phase 2 + // re-validates that the group still maps to the same workflow under the lock. + const preTable = await getTableById(data.tableId) + if (!preTable) throw new Error('Table not found') + const preGroup = (preTable.schema.workflowGroups ?? []).find((g) => g.id === data.groupId) + if (!preGroup) { + throw new Error(`Workflow group "${data.groupId}" not found`) + } + const workflowId = preGroup.workflowId + + const [ + { loadWorkflowFromNormalizedTables }, + { flattenWorkflowOutputs, getBlockExecutionOrder }, + { columnTypeForLeaf, deriveOutputColumnName }, + ] = await Promise.all([ + import('@/lib/workflows/persistence/utils'), + import('@/lib/workflows/blocks/flatten-outputs'), + import('@/lib/table/column-naming'), + ]) + const normalized = await loadWorkflowFromNormalizedTables(workflowId) + if (!normalized) { + throw new Error(`Workflow ${workflowId} not found`) + } + const blocks = Object.values(normalized.blocks ?? {}).map((b) => ({ + id: b.id, + type: b.type, + name: b.name, + triggerMode: (b as { triggerMode?: boolean }).triggerMode, + subBlocks: b.subBlocks as Record | undefined, + })) + const flattened = flattenWorkflowOutputs(blocks, normalized.edges ?? []) + const match = flattened.find((f) => f.blockId === data.blockId && f.path === data.path) + if (!match) { + throw new Error( + `Output ${data.blockId}::${data.path} is not a valid pickable output on workflow ${workflowId}` + ) + } + const newColumnType = columnTypeForLeaf(match.leafType) + const distances = getBlockExecutionOrder(blocks, normalized.edges ?? []) + const flatIndex = new Map(flattened.map((f, i) => [`${f.blockId}::${f.path}`, i])) + + // Phase 2 (locked): re-read fresh, validate against the current schema, and + // write. The critical section holds no I/O — just the in-memory splice + the + // schema UPDATE — so concurrent adders queue behind it quickly. + const { updatedTable, newOutput } = await withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const groups = schema.workflowGroups ?? [] + const groupIndex = groups.findIndex((g) => g.id === data.groupId) + if (groupIndex === -1) { + throw new Error(`Workflow group "${data.groupId}" not found`) + } + const group = groups[groupIndex] + if (group.workflowId !== workflowId) { + throw new Error( + `Workflow group "${data.groupId}" was remapped to a different workflow concurrently; retry the add.` + ) + } + + if (group.outputs.some((o) => o.blockId === data.blockId && o.path === data.path)) { + throw new Error( + `Workflow group "${data.groupId}" already has an output at ${data.blockId}::${data.path}` + ) + } + + const taken = new Set(schema.columns.map((c) => c.name)) + const columnName = data.columnName ?? deriveOutputColumnName(data.path, taken) + if (!NAME_PATTERN.test(columnName)) { + throw new Error(`Invalid column name "${columnName}". Must satisfy ${NAME_PATTERN.source}.`) + } + if (taken.has(columnName)) { + throw new Error(`Column "${columnName}" already exists`) + } + if (schema.columns.length + 1 > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { + throw new Error( + `Adding a column would exceed the maximum (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE}).` + ) + } + + const newColDef: ColumnDefinition = { + id: generateColumnId(), + name: columnName, + type: newColumnType, + required: false, + unique: false, + workflowGroupId: data.groupId, + } + const newColumnId = getColumnId(newColDef) + const newOutput: WorkflowGroupOutput = { + blockId: data.blockId, + path: data.path, + columnName: newColumnId, + } + + // Sort all of the group's outputs (existing + new) in workflow execution + // order: BFS distance from the start block ASC, with discovery order as + // tiebreak. This matches what the column-sidebar does at create time, so + // columns from the same workflow always read in the order their blocks run + // — regardless of whether they were added at create time or one-by-one. + const groupColIdsBefore = new Set(group.outputs.map((o) => o.columnName)) + const orderKey = (o: { blockId: string; path: string }) => { + const d = distances[o.blockId] + const dist = d === undefined || d < 0 ? Number.POSITIVE_INFINITY : d + const idx = flatIndex.get(`${o.blockId}::${o.path}`) ?? Number.POSITIVE_INFINITY + return [dist, idx] as const + } + const allGroupOutputs = [...group.outputs, newOutput].sort((a, b) => { + const [da, ia] = orderKey(a) + const [db, ib] = orderKey(b) + return da !== db ? da - db : ia - ib + }) + const orderedGroupColIds = allGroupOutputs.map((o) => o.columnName) + const updatedGroup: WorkflowGroup = { + ...group, + outputs: allGroupOutputs, + } + const nextGroups = groups.map((g, i) => (i === groupIndex ? updatedGroup : g)) + + // Splice the new column run into nextColumns: keep the columns outside the + // group where they were, replace the group's contiguous run with the + // BFS-ordered list. Anchor at the position of the first existing sibling + // (or append if the group was empty). + const colById = new Map(schema.columns.map((c) => [getColumnId(c), c])) + const orderedGroupCols: ColumnDefinition[] = orderedGroupColIds.map((id) => { + if (id === newColumnId) return newColDef + const existing = colById.get(id) + if (!existing) { + throw new Error(`Internal: column "${id}" missing while splicing group outputs`) + } + return existing + }) + const remainingCols = schema.columns.filter((c) => !groupColIdsBefore.has(getColumnId(c))) + const firstGroupIdx = schema.columns.findIndex((c) => groupColIdsBefore.has(getColumnId(c))) + const colAnchor = firstGroupIdx === -1 ? remainingCols.length : firstGroupIdx + const nextColumns = [ + ...remainingCols.slice(0, colAnchor), + ...orderedGroupCols, + ...remainingCols.slice(colAnchor), + ] + + const updatedSchema: TableSchema = { + ...schema, + columns: nextColumns, + workflowGroups: nextGroups, + } + + const updatedColumnOrder = table.metadata?.columnOrder + ? (() => { + const orderWithoutGroup = table.metadata!.columnOrder!.filter( + (id) => !groupColIdsBefore.has(id) + ) + const firstGroupOrderIdx = table.metadata!.columnOrder!.findIndex((id) => + groupColIdsBefore.has(id) + ) + const orderAnchor = + firstGroupOrderIdx === -1 ? orderWithoutGroup.length : firstGroupOrderIdx + return [ + ...orderWithoutGroup.slice(0, orderAnchor), + ...orderedGroupColIds, + ...orderWithoutGroup.slice(orderAnchor), + ] + })() + : undefined + + assertValidSchema(updatedSchema, updatedColumnOrder) + + const updatedMetadata: TableMetadata | null = + updatedColumnOrder && table.metadata + ? { ...table.metadata, columnOrder: updatedColumnOrder } + : table.metadata + ? { ...table.metadata } + : null + + const now = new Date() + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + + logger.info( + `[${requestId}] Added output "${columnName}" (${newColDef.type}) to workflow group "${data.groupId}" in table ${data.tableId}` + ) + + const updatedTable: TableDefinition = { + ...table, + schema: updatedSchema, + metadata: updatedMetadata, + updatedAt: now, + } + return { updatedTable, newOutput } + }) + + // Backfill from saved execution logs — same flow `updateWorkflowGroup` + // uses for added outputs. Reads each row's saved trace spans for the + // group's executionId and writes the new output's value back. Existing + // rows that have hand-edited values are left alone (overwrite: false). + // Cheap compared to re-running the workflow on every row, which is what + // an earlier version of this code did — that mistakenly fanned out N + // workflow-group-cell jobs and burned compute the user didn't ask for. + // Small tables backfill inline; large ones run as a background job. + // Lazy import: backfill-runner closes a cycle back to this module. + try { + const { maybeBackfillGroupOutputs } = await import('@/lib/table/backfill-runner') + await maybeBackfillGroupOutputs({ + table: updatedTable, + groupId: data.groupId, + outputs: [newOutput], + overwrite: false, + requestId, + actorUserId: data.actorUserId, + }) + } catch (err) { + logger.warn( + `[${requestId}] Backfill from execution logs failed for ${data.tableId} group ${data.groupId} after adding output "${newOutput.columnName}":`, + err + ) + } + + return updatedTable +} + +/** + * Removes a single output from a workflow group. Drops the bound column and + * strips the value from every row's `data` JSONB. If the output is the + * group's last, the empty group is left in place — drop it explicitly with + * `deleteWorkflowGroup` if needed. + */ +export async function deleteWorkflowGroupOutput( + data: { tableId: string; groupId: string; columnName: string }, + requestId: string +): Promise { + return withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const groups = schema.workflowGroups ?? [] + const groupIndex = groups.findIndex((g) => g.id === data.groupId) + if (groupIndex === -1) { + throw new Error(`Workflow group "${data.groupId}" not found`) + } + const group = groups[groupIndex] + // `data.columnName` may be a column id (first-party) or display name + // (mothership/legacy); resolve to the stable id used everywhere below. + const targetColumn = schema.columns.find((c) => columnMatchesRef(c, data.columnName)) + const columnId = targetColumn ? getColumnId(targetColumn) : data.columnName + if (!group.outputs.some((o) => o.columnName === columnId)) { + throw new Error( + `Workflow group "${data.groupId}" has no output bound to column "${data.columnName}"` + ) + } + + const updatedGroup: WorkflowGroup = { + ...group, + outputs: group.outputs.filter((o) => o.columnName !== columnId), + } + const nextGroups = groups.map((g, i) => (i === groupIndex ? updatedGroup : g)) + const nextColumns = schema.columns.filter((c) => getColumnId(c) !== columnId) + const updatedSchema: TableSchema = { + ...schema, + columns: nextColumns, + workflowGroups: nextGroups, + } + + const updatedColumnOrder = table.metadata?.columnOrder?.filter((id) => id !== columnId) + assertValidSchema(updatedSchema, updatedColumnOrder) + + const updatedMetadata: TableMetadata | null = + updatedColumnOrder && table.metadata + ? { ...table.metadata, columnOrder: updatedColumnOrder } + : table.metadata + ? { ...table.metadata } + : null + + const now = new Date() + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + await trx.execute( + sql`UPDATE user_table_rows SET data = data - ${columnId}::text WHERE table_id = ${data.tableId} AND data ? ${columnId}::text` + ) + + logger.info( + `[${requestId}] Removed output "${data.columnName}" from workflow group "${data.groupId}" in table ${data.tableId}` + ) + + return { ...table, schema: updatedSchema, metadata: updatedMetadata, updatedAt: now } + }) +} + +/** + * Removes a workflow group plus all its output columns. Also strips the + * group's `executions[groupId]` entry from every row. + */ +export async function deleteWorkflowGroup( + data: DeleteWorkflowGroupData, + requestId: string +): Promise { + return withLockedTable(data.tableId, async (table, trx) => { + const schema = table.schema + const groups = schema.workflowGroups ?? [] + const group = groups.find((g) => g.id === data.groupId) + if (!group) { + throw new Error(`Workflow group "${data.groupId}" not found`) + } + + const removedColumnIds = new Set(group.outputs.map((o) => o.columnName)) + // Removed group's output columns may be referenced as deps by sibling groups. + // Strip those refs so we don't leave dangling-column deps behind. + const nextGroups = groups + .filter((g) => g.id !== data.groupId) + .map((g) => stripGroupDeps(g, removedColumnIds)) + const updatedSchema: TableSchema = { + ...schema, + columns: schema.columns.filter((c) => !removedColumnIds.has(getColumnId(c))), + workflowGroups: nextGroups, + } + const updatedColumnOrder = table.metadata?.columnOrder?.filter( + (id) => !removedColumnIds.has(id) + ) + assertValidSchema(updatedSchema, updatedColumnOrder) + + const updatedMetadata: TableMetadata | null = + updatedColumnOrder && table.metadata + ? { ...table.metadata, columnOrder: updatedColumnOrder } + : table.metadata + ? { ...table.metadata } + : null + + const now = new Date() + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + await trx + .update(userTableDefinitions) + .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) + .where(eq(userTableDefinitions.id, data.tableId)) + for (const id of removedColumnIds) { + await trx.execute( + sql`UPDATE user_table_rows SET data = data - ${id}::text WHERE table_id = ${data.tableId} AND data ? ${id}::text` + ) + } + await stripGroupExecutions(trx, data.tableId, [data.groupId]) + + logger.info( + `[${requestId}] Deleted workflow group "${data.groupId}" from table ${data.tableId}` + ) + + return { + ...table, + schema: updatedSchema, + metadata: updatedMetadata, + updatedAt: now, + } + }) +} diff --git a/apps/sim/lib/workflows/deployment-outbox.ts b/apps/sim/lib/workflows/deployment-outbox.ts index 468e621f106..6dcb0b9fe4a 100644 --- a/apps/sim/lib/workflows/deployment-outbox.ts +++ b/apps/sim/lib/workflows/deployment-outbox.ts @@ -615,7 +615,7 @@ async function pruneWorkflowGroupOutputsIfStillActive(params: { if (!versionRow) return - const { pruneStaleWorkflowGroupOutputs } = await import('@/lib/table/service') + const { pruneStaleWorkflowGroupOutputs } = await import('@/lib/table/workflow-groups/service') await pruneStaleWorkflowGroupOutputs({ workflowId: params.workflowId, workspaceId: params.workspaceId, From 3e2b641e9dc15da6f201d51015a54c0f68695335 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 16:24:15 -0700 Subject: [PATCH 05/24] fix(credential-sets): stop leaking open-invite tokens to all users (#5074) GET /api/credential-sets/invitations returned every pending, unexpired link-only (null-email) invitation across all organizations, including the bearer token. Any authenticated user could enumerate and accept another org's invitation, joining its credential set (cross-tenant access). Scope the listing strictly to invitations addressed to the caller's own email. Open-link invites remain redeemable only via the out-of-band /credential-account/[token] URL. --- apps/sim/app/api/credential-sets/invitations/route.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/credential-sets/invitations/route.ts b/apps/sim/app/api/credential-sets/invitations/route.ts index 2ad4eb23d11..a6da9ca82b2 100644 --- a/apps/sim/app/api/credential-sets/invitations/route.ts +++ b/apps/sim/app/api/credential-sets/invitations/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { credentialSet, credentialSetInvitation, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, gt, isNull, or } from 'drizzle-orm' +import { and, eq, gt } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -16,6 +16,9 @@ export const GET = withRouteHandler(async () => { } try { + // Scope to invitations addressed to the caller's own email only. Open-link + // (null-email) invites carry a bearer token redeemed via the out-of-band URL + // and must never be listed, or any user could accept another org's invite. const invitations = await db .select({ invitationId: credentialSetInvitation.id, @@ -37,10 +40,7 @@ export const GET = withRouteHandler(async () => { .leftJoin(user, eq(credentialSetInvitation.invitedBy, user.id)) .where( and( - or( - eq(credentialSetInvitation.email, session.user.email), - isNull(credentialSetInvitation.email) - ), + eq(credentialSetInvitation.email, session.user.email), eq(credentialSetInvitation.status, 'pending'), gt(credentialSetInvitation.expiresAt, new Date()) ) From d538b76eda5b15da135e6e638f1db4e068480f64 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:32:35 -0700 Subject: [PATCH 06/24] feat(copilot): server-side mothership tool/vfs/file metrics (#5071) --- apps/sim/instrumentation-node.ts | 19 +- apps/sim/lib/copilot/chat/payload.ts | 4 +- apps/sim/lib/copilot/chat/post.ts | 5 +- apps/sim/lib/copilot/generated/metrics-v1.ts | 58 ++++++ .../lib/copilot/generated/tool-schemas-v1.ts | 196 +++++++++--------- .../copilot/generated/trace-attributes-v1.ts | 50 ++++- .../lib/copilot/generated/trace-events-v1.ts | 4 +- apps/sim/lib/copilot/request/metrics.ts | 101 +++++++++ .../sim/lib/copilot/request/tools/executor.ts | 34 ++- apps/sim/lib/copilot/vfs/file-reader.ts | 9 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 18 +- package.json | 2 + scripts/generate-mship-contracts.ts | 1 + scripts/sync-metrics-contract.ts | 142 +++++++++++++ 14 files changed, 518 insertions(+), 125 deletions(-) create mode 100644 apps/sim/lib/copilot/generated/metrics-v1.ts create mode 100644 apps/sim/lib/copilot/request/metrics.ts create mode 100644 scripts/sync-metrics-contract.ts diff --git a/apps/sim/instrumentation-node.ts b/apps/sim/instrumentation-node.ts index e52eb884a9e..9d536bb5276 100644 --- a/apps/sim/instrumentation-node.ts +++ b/apps/sim/instrumentation-node.ts @@ -83,6 +83,17 @@ function normalizeOtlpMetricsUrl(url: string): string { } } +// deployment.environment in the GO value space (dev | staging | prod) without +// any new infra env var. Every deployed Sim tier already gets +// APPCONFIG_ENVIRONMENT = the infra env name (dev | staging | production), so we +// reuse it and map production -> prod to match Go's `OTEL_DEPLOYMENT_ENVIRONMENT` +// (and thus a single Grafana $env filter spans Sim + Go). Returns undefined when +// unset (local dev) so the OTEL_/NODE_ENV fallbacks still apply. +function deploymentEnvFromAppConfig(v: string | undefined): string | undefined { + if (!v) return undefined + return v === 'production' ? 'prod' : v +} + // Sampling ratio from env (mirrors Go's `samplerFromEnv`); fallback // is 100% everywhere. Retention caps cost, not sampling. function resolveSamplingRatio(_isLocalEndpoint: boolean): number { @@ -270,11 +281,15 @@ async function initializeOpenTelemetry() { resourceFromAttributes({ [ATTR_SERVICE_NAME]: telemetryConfig.serviceName, [ATTR_SERVICE_VERSION]: telemetryConfig.serviceVersion, - // OTEL_ → DEPLOYMENT_ENVIRONMENT → NODE_ENV; matches Go's - // `resourceEnvFromEnv()` so both halves tag the same value. + // OTEL_ → DEPLOYMENT_ENVIRONMENT → APPCONFIG_ENVIRONMENT (mapped to the + // Go value space) → NODE_ENV. Matches Go's `resourceEnvFromEnv()` so a + // single $env spans Sim + Go. APPCONFIG_ENVIRONMENT (already set on every + // deployed tier) is the fix that stops deployed Sim tagging everything + // "production" via the NODE_ENV fallback — no new infra env var needed. [ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.OTEL_DEPLOYMENT_ENVIRONMENT || process.env.DEPLOYMENT_ENVIRONMENT || + deploymentEnvFromAppConfig(process.env.APPCONFIG_ENVIRONMENT) || env.NODE_ENV || 'development', 'service.namespace': 'mothership', diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 31da4396bc1..6c5e881e54f 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -37,6 +37,7 @@ interface BuildPayloadParams { userTimezone?: string userMetadata?: { name?: string + email?: string timezone?: string } includeMothershipTools?: boolean @@ -367,7 +368,8 @@ export async function buildCopilotRequestPayload( ...(params.workspaceContext ? { workspaceContext: params.workspaceContext } : {}), ...(params.userPermission ? { userPermission: params.userPermission } : {}), ...(params.userTimezone ? { userTimezone: params.userTimezone } : {}), - ...(params.userMetadata && (params.userMetadata.name || params.userMetadata.timezone) + ...(params.userMetadata && + (params.userMetadata.name || params.userMetadata.email || params.userMetadata.timezone) ? { userMetadata: params.userMetadata } : {}), // Tell the copilot file subagent which document toolchain to write. Emitted diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 5465b25437a..721aa8b5519 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -169,7 +169,7 @@ type UnifiedChatBranch = fileAttachments?: UnifiedChatRequest['fileAttachments'] userPermission?: string userTimezone?: string - userMetadata?: { name?: string; timezone?: string } + userMetadata?: { name?: string; email?: string; timezone?: string } workflowId: string workflowName?: string workspaceId?: string @@ -204,7 +204,7 @@ type UnifiedChatBranch = fileAttachments?: UnifiedChatRequest['fileAttachments'] userPermission?: string userTimezone?: string - userMetadata?: { name?: string; timezone?: string } + userMetadata?: { name?: string; email?: string; timezone?: string } workspaceContext?: string }) => Promise> buildExecutionContext: (params: { @@ -722,6 +722,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { const body = ChatMessageSchema.parse(await req.json()) const userMetadata = { ...(authenticatedUserName ? { name: authenticatedUserName } : {}), + ...(authenticatedUserEmail ? { email: authenticatedUserEmail } : {}), ...(body.userTimezone ? { timezone: body.userTimezone } : {}), } const normalizedContexts = normalizeContexts(body.contexts) ?? [] diff --git a/apps/sim/lib/copilot/generated/metrics-v1.ts b/apps/sim/lib/copilot/generated/metrics-v1.ts new file mode 100644 index 00000000000..dd8527f8158 --- /dev/null +++ b/apps/sim/lib/copilot/generated/metrics-v1.ts @@ -0,0 +1,58 @@ +// AUTO-GENERATED FILE. DO NOT EDIT. +// +// Source: copilot/copilot/contracts/metrics-v1.schema.json +// Regenerate with: bun run metrics-contract:generate +// +// Canonical mothership OTel metric names. Call sites should reference +// `Metric.` (e.g. `Metric.CopilotToolDuration`) rather than raw +// string literals, so the Go-side contract is the single source of truth and +// typos become compile errors. +// +// NAMES ONLY. Label keys and histogram bucket boundaries are NOT in this +// contract — Go owns the label-cardinality allowlist and the shared bucket +// constant, and the Sim emitter MUST mirror those by hand so the Go∪Sim metric +// union is queryable as one series set. + +export const Metric = { + CopilotCacheAttempted: 'copilot.cache.attempted', + CopilotCacheHit: 'copilot.cache.hit', + CopilotCacheWrite: 'copilot.cache.write', + CopilotFileReadDuration: 'copilot.file.read.duration', + CopilotFileReadSize: 'copilot.file.read.size', + CopilotMessagesSerializeDuration: 'copilot.messages.serialize.duration', + CopilotRequestCount: 'copilot.request.count', + CopilotRequestDuration: 'copilot.request.duration', + CopilotToolCalls: 'copilot.tool.calls', + CopilotToolDuration: 'copilot.tool.duration', + CopilotVfsMaterializeDuration: 'copilot.vfs.materialize.duration', + GenAiClientCacheTokenUsage: 'gen_ai.client.cache.token.usage', + GenAiClientTokenUsage: 'gen_ai.client.token.usage', + LlmClientErrors: 'llm.client.errors', + LlmClientOutputCutoff: 'llm.client.output_cutoff', + LlmClientStreamDuration: 'llm.client.stream.duration', + LlmClientTimeToFirstToken: 'llm.client.time_to_first_token', +} as const + +export type MetricKey = keyof typeof Metric +export type MetricValue = (typeof Metric)[MetricKey] + +/** Readonly sorted list of every canonical mothership metric name. */ +export const MetricValues: readonly MetricValue[] = [ + 'copilot.cache.attempted', + 'copilot.cache.hit', + 'copilot.cache.write', + 'copilot.file.read.duration', + 'copilot.file.read.size', + 'copilot.messages.serialize.duration', + 'copilot.request.count', + 'copilot.request.duration', + 'copilot.tool.calls', + 'copilot.tool.duration', + 'copilot.vfs.materialize.duration', + 'gen_ai.client.cache.token.usage', + 'gen_ai.client.token.usage', + 'llm.client.errors', + 'llm.client.output_cutoff', + 'llm.client.stream.duration', + 'llm.client.time_to_first_token', +] as const diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index a93837798b1..cd6421a0310 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -96,7 +96,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -162,7 +162,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_file_folder']: { + create_file_folder: { parameters: { type: 'object', properties: { @@ -180,7 +180,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -201,7 +201,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -226,7 +226,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -259,7 +259,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -289,7 +289,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_file_folder']: { + delete_file_folder: { parameters: { type: 'object', properties: { @@ -305,7 +305,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -321,7 +321,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -337,7 +337,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -350,7 +350,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -364,7 +364,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -448,7 +448,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -607,7 +607,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -723,7 +723,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['diff_workflows']: { + diff_workflows: { parameters: { type: 'object', properties: { @@ -747,7 +747,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -796,7 +796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -828,7 +828,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -867,7 +867,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['enrichment_run']: { + enrichment_run: { parameters: { type: 'object', properties: { @@ -911,7 +911,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['matched', 'result'], }, }, - ['ffmpeg']: { + ffmpeg: { parameters: { type: 'object', properties: { @@ -1092,7 +1092,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { properties: { prompt: { @@ -1105,7 +1105,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -1243,7 +1243,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -1261,7 +1261,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_audio']: { + generate_audio: { parameters: { type: 'object', properties: { @@ -1413,7 +1413,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -1541,7 +1541,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_video']: { + generate_video: { parameters: { type: 'object', properties: { @@ -1708,7 +1708,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -1729,7 +1729,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1751,7 +1751,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1764,7 +1764,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_log']: { + get_deployment_log: { parameters: { type: 'object', properties: { @@ -1777,7 +1777,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1802,7 +1802,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1830,14 +1830,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1856,7 +1856,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_run_options']: { + get_workflow_run_options: { parameters: { type: 'object', properties: { @@ -1869,7 +1869,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1888,7 +1888,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1936,7 +1936,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1949,7 +1949,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1962,7 +1962,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -2155,7 +2155,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_file_folders']: { + list_file_folders: { parameters: { type: 'object', properties: { @@ -2167,7 +2167,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -2179,7 +2179,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_integration_tools']: { + list_integration_tools: { parameters: { properties: { integration: { @@ -2193,14 +2193,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -2213,7 +2213,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_deployment']: { + load_deployment: { parameters: { type: 'object', properties: { @@ -2232,7 +2232,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_integration_tool']: { + load_integration_tool: { parameters: { properties: { tool_ids: { @@ -2249,7 +2249,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -2278,7 +2278,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -2358,7 +2358,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -2433,7 +2433,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -2485,7 +2485,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -2518,7 +2518,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -2542,7 +2542,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['media']: { + media: { parameters: { properties: { prompt: { @@ -2555,7 +2555,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file']: { + move_file: { parameters: { type: 'object', properties: { @@ -2576,7 +2576,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file_folder']: { + move_file_folder: { parameters: { type: 'object', properties: { @@ -2594,7 +2594,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -2612,7 +2612,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -2632,7 +2632,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -2646,7 +2646,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -2660,7 +2660,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -2694,7 +2694,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['promote_to_live']: { + promote_to_live: { parameters: { type: 'object', properties: { @@ -2713,7 +2713,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['query_logs']: { + query_logs: { parameters: { type: 'object', properties: { @@ -2824,7 +2824,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -2851,7 +2851,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -2930,7 +2930,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2966,7 +2966,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_file_folder']: { + rename_file_folder: { parameters: { type: 'object', properties: { @@ -2983,7 +2983,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -3000,7 +3000,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -3013,7 +3013,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -3036,7 +3036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -3054,7 +3054,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -3071,7 +3071,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -3103,7 +3103,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -3135,7 +3135,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -3173,7 +3173,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -3216,7 +3216,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -3237,7 +3237,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -3254,7 +3254,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -3275,7 +3275,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -3315,7 +3315,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -3337,7 +3337,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -3359,7 +3359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -3393,7 +3393,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -3434,7 +3434,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -3448,7 +3448,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -3461,7 +3461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_deployment_version']: { + update_deployment_version: { parameters: { type: 'object', properties: { @@ -3490,7 +3490,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -3508,7 +3508,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -3533,7 +3533,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -3582,7 +3582,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -3944,7 +3944,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { properties: { prompt: { @@ -3957,7 +3957,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts index 441ec59d16d..982857673f5 100644 --- a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts @@ -54,10 +54,8 @@ export const TraceAttr = { AuthProvider: 'auth.provider', AuthValidateStatusCode: 'auth.validate.status_code', AwsRegion: 'aws.region', - BedrockErrorCode: 'bedrock.error_code', - BedrockModelId: 'bedrock.model_id', - BedrockRequestBodyBytesRetry: 'bedrock.request.body_bytes_retry', BillingAttempts: 'billing.attempts', + BillingByok: 'billing.byok', BillingChangeType: 'billing.change_type', BillingCostInputUsd: 'billing.cost.input_usd', BillingCostOutputUsd: 'billing.cost.output_usd', @@ -159,6 +157,14 @@ export const TraceAttr = { ContextReduced: 'context.reduced', ContextSummarizeInputChars: 'context.summarize.input_chars', ContextSummarizeOutputChars: 'context.summarize.output_chars', + ContextTransformCaller: 'context.transform.caller', + ContextTransformCharsIn: 'context.transform.chars_in', + ContextTransformCharsOut: 'context.transform.chars_out', + ContextTransformDropCount: 'context.transform.drop_count', + ContextTransformDrops: 'context.transform.drops', + ContextTransformMessagesIn: 'context.transform.messages_in', + ContextTransformMessagesOut: 'context.transform.messages_out', + ContextTransformStage: 'context.transform.stage', CopilotAbortControllerFired: 'copilot.abort.controller_fired', CopilotAbortGoMarkerOk: 'copilot.abort.go_marker_ok', CopilotAbortLocalAborted: 'copilot.abort.local_aborted', @@ -276,6 +282,7 @@ export const TraceAttr = { CopilotVfsOutcome: 'copilot.vfs.outcome', CopilotVfsOutputBytes: 'copilot.vfs.output.bytes', CopilotVfsOutputMediaType: 'copilot.vfs.output.media_type', + CopilotVfsPhase: 'copilot.vfs.phase', CopilotVfsReadImageResized: 'copilot.vfs.read.image.resized', CopilotVfsReadOutcome: 'copilot.vfs.read.outcome', CopilotVfsReadOutputBytes: 'copilot.vfs.read.output.bytes', @@ -389,6 +396,7 @@ export const TraceAttr = { GenAiRequestToolUseBlocks: 'gen_ai.request.tool_use_blocks', GenAiRequestToolsCount: 'gen_ai.request.tools.count', GenAiRequestUserMessages: 'gen_ai.request.user_messages', + GenAiResponseFinishReasons: 'gen_ai.response.finish_reasons', GenAiResponseModel: 'gen_ai.response.model', GenAiStreamPhaseTextBytes: 'gen_ai.stream.phase.text.bytes', GenAiStreamPhaseTextChunks: 'gen_ai.stream.phase.text.chunks', @@ -434,7 +442,9 @@ export const TraceAttr = { InvitationRole: 'invitation.role', KnowledgeBaseId: 'knowledge_base.id', KnowledgeBaseName: 'knowledge_base.name', + LlmBackend: 'llm.backend', LlmErrorStage: 'llm.error_stage', + LlmProtocol: 'llm.protocol', LlmRequestBodyBytes: 'llm.request.body_bytes', LlmStreamBytes: 'llm.stream.bytes', LlmStreamChunks: 'llm.stream.chunks', @@ -460,6 +470,10 @@ export const TraceAttr = { MemoryPath: 'memory.path', MemoryRowCount: 'memory.row_count', MessageId: 'message.id', + MessagesDeserializeMs: 'messages.deserialize_ms', + MessagesSerializeOp: 'messages.serialize.op', + MessagesSerializeSite: 'messages.serialize.site', + MessagesSerializeMs: 'messages.serialize_ms', MessagingDestinationName: 'messaging.destination.name', MessagingSystem: 'messaging.system', ModelDurationMs: 'model.duration_ms', @@ -495,14 +509,15 @@ export const TraceAttr = { ResumeResultsFailureCount: 'resume.results.failure_count', ResumeResultsSuccessCount: 'resume.results.success_count', RouterBackendName: 'router.backend_name', - RouterBedrockEnabled: 'router.bedrock_enabled', - RouterBedrockSupportedModel: 'router.bedrock_supported_model', + RouterConfigVersion: 'router.config_version', RouterId: 'router.id', RouterName: 'router.name', + RouterRouteReason: 'router.route_reason', RouterSelectedBackend: 'router.selected_backend', RouterSelectedPath: 'router.selected_path', RunId: 'run.id', SearchResultsCount: 'search.results_count', + ServerAddress: 'server.address', ServiceInstanceId: 'service.instance.id', ServiceName: 'service.name', ServiceNamespace: 'service.namespace', @@ -663,10 +678,8 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'auth.provider', 'auth.validate.status_code', 'aws.region', - 'bedrock.error_code', - 'bedrock.model_id', - 'bedrock.request.body_bytes_retry', 'billing.attempts', + 'billing.byok', 'billing.change_type', 'billing.cost.input_usd', 'billing.cost.output_usd', @@ -768,6 +781,14 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'context.reduced', 'context.summarize.input_chars', 'context.summarize.output_chars', + 'context.transform.caller', + 'context.transform.chars_in', + 'context.transform.chars_out', + 'context.transform.drop_count', + 'context.transform.drops', + 'context.transform.messages_in', + 'context.transform.messages_out', + 'context.transform.stage', 'copilot.abort.controller_fired', 'copilot.abort.go_marker_ok', 'copilot.abort.local_aborted', @@ -885,6 +906,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'copilot.vfs.outcome', 'copilot.vfs.output.bytes', 'copilot.vfs.output.media_type', + 'copilot.vfs.phase', 'copilot.vfs.read.image.resized', 'copilot.vfs.read.outcome', 'copilot.vfs.read.output.bytes', @@ -987,6 +1009,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'gen_ai.request.tool_use_blocks', 'gen_ai.request.tools.count', 'gen_ai.request.user_messages', + 'gen_ai.response.finish_reasons', 'gen_ai.response.model', 'gen_ai.stream.phase.text.bytes', 'gen_ai.stream.phase.text.chunks', @@ -1032,7 +1055,9 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'invitation.role', 'knowledge_base.id', 'knowledge_base.name', + 'llm.backend', 'llm.error_stage', + 'llm.protocol', 'llm.request.body_bytes', 'llm.stream.bytes', 'llm.stream.chunks', @@ -1058,6 +1083,10 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'memory.path', 'memory.row_count', 'message.id', + 'messages.deserialize_ms', + 'messages.serialize.op', + 'messages.serialize.site', + 'messages.serialize_ms', 'messaging.destination.name', 'messaging.system', 'model.duration_ms', @@ -1093,14 +1122,15 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'resume.results.failure_count', 'resume.results.success_count', 'router.backend_name', - 'router.bedrock_enabled', - 'router.bedrock_supported_model', + 'router.config_version', 'router.id', 'router.name', + 'router.route_reason', 'router.selected_backend', 'router.selected_path', 'run.id', 'search.results_count', + 'server.address', 'service.instance.id', 'service.name', 'service.namespace', diff --git a/apps/sim/lib/copilot/generated/trace-events-v1.ts b/apps/sim/lib/copilot/generated/trace-events-v1.ts index 345606eff40..ffc4c17361d 100644 --- a/apps/sim/lib/copilot/generated/trace-events-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-events-v1.ts @@ -10,7 +10,7 @@ // become compile errors. export const TraceEvent = { - BedrockInvokeRetryWithoutImages: 'bedrock.invoke.retry_without_images', + ContextTransform: 'context.transform', CopilotOutputFileError: 'copilot.output_file.error', CopilotSseFirstEvent: 'copilot.sse.first_event', CopilotSseIdleGapExceeded: 'copilot.sse.idle_gap_exceeded', @@ -33,7 +33,7 @@ export type TraceEventValue = (typeof TraceEvent)[TraceEventKey] /** Readonly sorted list of every canonical event name. */ export const TraceEventValues: readonly TraceEventValue[] = [ - 'bedrock.invoke.retry_without_images', + 'context.transform', 'copilot.output_file.error', 'copilot.sse.first_event', 'copilot.sse.idle_gap_exceeded', diff --git a/apps/sim/lib/copilot/request/metrics.ts b/apps/sim/lib/copilot/request/metrics.ts new file mode 100644 index 00000000000..31f0e7996f6 --- /dev/null +++ b/apps/sim/lib/copilot/request/metrics.ts @@ -0,0 +1,101 @@ +// Sim server-side copilot metrics (U17). Sim's MeterProvider is wired in +// instrumentation-node.ts (OTLP → Mimir, 60s) but had no copilot instruments; +// this module is its first consumer. We emit the SAME metric names + label keys +// + histogram bucket boundaries as the Go side (copilot internal/telemetry + +// contracts/metrics_v1.go) so the Go∪Sim union is queryable as one series set +// — e.g. `copilot.tool.duration` split by `tool.executor` (go|client|sim). +// +// Bounded cardinality only: tool.name is capped to the shared tool catalog +// (else "other"); vfs phase / file-read outcome are bounded sets. NEVER a +// user/chat/request id (those explode Prometheus series). +import { type Counter, type Histogram, metrics } from '@opentelemetry/api' +import { Metric } from '@/lib/copilot/generated/metrics-v1' +import { TOOL_CATALOG } from '@/lib/copilot/generated/tool-catalog-v1' +import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' + +// MUST match Go's copilot/internal/telemetry/metrics.go LatencyBucketsMs +// exactly — a histogram_quantile(sum by (le) …) over the Go∪Sim union is only +// valid with identical boundaries. If you change one side, change the other. +const LATENCY_BUCKETS_MS = [ + 50, 100, 250, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 120000, 300000, +] + +// File sizes span KB→tens of MB; a bytes-appropriate bucket set (not latency). +const BYTE_BUCKETS = [1024, 8192, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456] + +interface CopilotMeterInstruments { + toolDuration: Histogram + toolCalls: Counter + vfsMaterializeDuration: Histogram + fileReadDuration: Histogram + fileReadBytes: Histogram +} + +let cached: CopilotMeterInstruments | undefined + +// Lazy init: Turbopack/Next can evaluate this module before the NodeSDK +// installs the real MeterProvider, so resolve instruments on first use (a +// no-op meter before then simply drops records — same pattern as getCopilotTracer). +function instruments(): CopilotMeterInstruments { + if (cached) return cached + const meter = metrics.getMeter('sim-copilot') + cached = { + toolDuration: meter.createHistogram(Metric.CopilotToolDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + toolCalls: meter.createCounter(Metric.CopilotToolCalls), + vfsMaterializeDuration: meter.createHistogram(Metric.CopilotVfsMaterializeDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + fileReadDuration: meter.createHistogram(Metric.CopilotFileReadDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + fileReadBytes: meter.createHistogram(Metric.CopilotFileReadSize, { + unit: 'By', + advice: { explicitBucketBoundaries: BYTE_BUCKETS }, + }), + } + return cached +} + +// Caps tool.name to the shared catalog (matches Go's cappedToolName): a +// catalog tool keeps its name, everything else (user MCP/custom/unknown) +// collapses to "other" so series count stays finite. +function cappedToolName(name: string): string { + return TOOL_CATALOG[name] ? name : 'other' +} + +// recordSimToolMetric emits copilot.tool.calls (+1) and copilot.tool.duration +// for one server-side Sim tool dispatch (executor=sim). outcome is the bounded +// tool outcome (success/error/…). Pure telemetry. +export function recordSimToolMetric(name: string, outcome: string, durationMs: number): void { + const { toolDuration, toolCalls } = instruments() + const attrs = { + [TraceAttr.ToolName]: cappedToolName(name), + [TraceAttr.ToolExecutor]: 'sim', + [TraceAttr.ToolOutcome]: outcome, + } + toolCalls.add(1, attrs) + if (durationMs >= 0) toolDuration.record(durationMs, attrs) +} + +// recordVfsMaterialize records VFS materialization time. Call once per phase +// with that phase's duration and once with phase="total" for the whole op, so +// the dashboard can show total + per-phase. phase must be a bounded value. +export function recordVfsMaterialize(phase: string, durationMs: number): void { + if (durationMs < 0) return + instruments().vfsMaterializeDuration.record(durationMs, { + [TraceAttr.CopilotVfsPhase]: phase, + }) +} + +// recordFileRead records server-side file-read duration + size by outcome. +export function recordFileRead(outcome: string, durationMs: number, bytes: number): void { + const { fileReadDuration, fileReadBytes } = instruments() + const attrs = { [TraceAttr.CopilotVfsReadOutcome]: outcome } + if (durationMs >= 0) fileReadDuration.record(durationMs, attrs) + if (bytes >= 0) fileReadBytes.record(bytes, attrs) +} diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index b9a78eae0bf..b1637b017ce 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -43,6 +43,7 @@ import { } from '@/lib/copilot/generated/tool-catalog-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { publishToolConfirmation } from '@/lib/copilot/persistence/tool-confirm' +import { recordSimToolMetric } from '@/lib/copilot/request/metrics' import { withCopilotToolSpan } from '@/lib/copilot/request/otel' import { markToolResultSeen } from '@/lib/copilot/request/sse-utils' import { @@ -397,15 +398,32 @@ export async function executeToolAndReport( argsPreview: argsPayload?.slice(0, 200), }, async (otelSpan) => { - const completion = await executeToolAndReportInner(toolCall, context, execContext, options) - otelSpan.setAttribute(TraceAttr.ToolOutcome, completion.status) - if (completion.message) { - otelSpan.setAttribute( - TraceAttr.ToolOutcomeMessage, - String(completion.message).slice(0, 500) - ) + const startedAt = Date.now() + try { + const completion = await executeToolAndReportInner(toolCall, context, execContext, options) + const durationMs = Date.now() - startedAt + otelSpan.setAttribute(TraceAttr.ToolOutcome, completion.status) + otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) + if (completion.message) { + otelSpan.setAttribute( + TraceAttr.ToolOutcomeMessage, + String(completion.message).slice(0, 500) + ) + } + // Durable Grafana signal for "which Sim tool is slowest" (executor=sim); + // pairs with the Go executor-boundary metric (U15) as one series set. + recordSimToolMetric(toolCall.name, completion.status, durationMs) + return completion + } catch (err) { + // executeToolAndReportInner threw (infra/unexpected error, not a normal + // 'error' completion). Still stamp the span + record the dispatch so + // copilot.tool.* isn't silently biased toward successful calls. + const durationMs = Date.now() - startedAt + otelSpan.setAttribute(TraceAttr.ToolOutcome, 'error') + otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) + recordSimToolMetric(toolCall.name, 'error', durationMs) + throw err } - return completion } ) } diff --git a/apps/sim/lib/copilot/vfs/file-reader.ts b/apps/sim/lib/copilot/vfs/file-reader.ts index f2d65252a27..26388d5621a 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.ts @@ -9,6 +9,7 @@ import { import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' +import { recordFileRead } from '@/lib/copilot/request/metrics' import { markSpanForError } from '@/lib/copilot/request/otel' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -284,7 +285,8 @@ export interface FileReadResult { * nests underneath for the image-resize path. */ export async function readFileRecord(record: WorkspaceFileRecord): Promise { - return getVfsTracer().startActiveSpan( + const startedAt = Date.now() + const result = await getVfsTracer().startActiveSpan( TraceSpan.CopilotVfsReadFile, { attributes: { @@ -414,4 +416,9 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise): string[] { + const defs = (schema.$defs ?? {}) as Record + const nameDef = defs.MetricsV1Name + if ( + !nameDef || + typeof nameDef !== 'object' || + !Array.isArray((nameDef as Record).enum) + ) { + throw new Error('metrics-v1.schema.json is missing $defs.MetricsV1Name.enum') + } + const enumValues = (nameDef as Record).enum as unknown[] + if (!enumValues.every((v) => typeof v === 'string')) { + throw new Error('MetricsV1Name enum must be string-only') + } + return (enumValues as string[]).slice().sort() +} + +/** + * Convert a wire metric name like `copilot.request.duration` into an + * identifier-safe PascalCase key like `CopilotRequestDuration`. Same algorithm + * as the trace-attributes sync script so readers learn one and reuse it. + */ +function toIdentifier(name: string): string { + const parts = name.split(/[^A-Za-z0-9]+/).filter(Boolean) + if (parts.length === 0) { + throw new Error(`Cannot derive identifier for metric name: ${name}`) + } + const ident = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join('') + if (/^[0-9]/.test(ident)) { + throw new Error(`Derived identifier "${ident}" for metric "${name}" starts with a digit`) + } + return ident +} + +function render(metricNames: string[]): string { + const pairs = metricNames.map((name) => ({ name, ident: toIdentifier(name) })) + + const seen = new Map() + for (const p of pairs) { + const prev = seen.get(p.ident) + if (prev && prev !== p.name) { + throw new Error(`Identifier collision: "${prev}" and "${p.name}" both map to "${p.ident}"`) + } + seen.set(p.ident, p.name) + } + + const constLines = pairs.map((p) => ` ${p.ident}: ${JSON.stringify(p.name)},`).join('\n') + const arrayEntries = metricNames.map((n) => ` ${JSON.stringify(n)},`).join('\n') + + return `// AUTO-GENERATED FILE. DO NOT EDIT. +// +// Source: copilot/copilot/contracts/metrics-v1.schema.json +// Regenerate with: bun run metrics-contract:generate +// +// Canonical mothership OTel metric names. Call sites should reference +// \`Metric.\` (e.g. \`Metric.CopilotToolDuration\`) rather than raw +// string literals, so the Go-side contract is the single source of truth and +// typos become compile errors. +// +// NAMES ONLY. Label keys and histogram bucket boundaries are NOT in this +// contract — Go owns the label-cardinality allowlist and the shared bucket +// constant, and the Sim emitter MUST mirror those by hand so the Go∪Sim metric +// union is queryable as one series set. + +export const Metric = { +${constLines} +} as const; + +export type MetricKey = keyof typeof Metric; +export type MetricValue = (typeof Metric)[MetricKey]; + +/** Readonly sorted list of every canonical mothership metric name. */ +export const MetricValues: readonly MetricValue[] = [ +${arrayEntries} +] as const; +` +} + +async function main() { + const checkOnly = process.argv.includes('--check') + const inputArg = process.argv.find((a) => a.startsWith('--input=')) + const inputPath = inputArg + ? resolve(ROOT, inputArg.slice('--input='.length)) + : DEFAULT_CONTRACT_PATH + + const raw = await readFile(inputPath, 'utf8') + const schema = JSON.parse(raw) + const metricNames = extractMetricNames(schema) + const rendered = formatGeneratedSource(render(metricNames), OUTPUT_PATH, ROOT) + + if (checkOnly) { + const existing = await readFile(OUTPUT_PATH, 'utf8').catch(() => null) + if (existing !== rendered) { + throw new Error('Generated metrics contract is stale. Run: bun run metrics-contract:generate') + } + console.log('Metrics contract is up to date.') + return + } + + await mkdir(dirname(OUTPUT_PATH), { recursive: true }) + await writeFile(OUTPUT_PATH, rendered, 'utf8') + console.log(`Generated metrics types -> ${OUTPUT_PATH}`) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) From dd32abef1e2043c787799ce9722802630ae8c432 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 16:40:24 -0700 Subject: [PATCH 07/24] feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management (#5072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management Add nine JSM Assets tools so workflows can read and write Atlassian Assets (Insight/CMDB) objects — the foundation for keeping JSM asset tables in sync for software/hardware asset management. Tools (wired into the Jira Service Management block): - jsm_list_object_schemas, jsm_get_object_schema - jsm_list_object_types, jsm_get_object_type_attributes - jsm_search_objects_aql (AQL search with pagination) - jsm_get_object, jsm_create_object, jsm_update_object, jsm_delete_object Each tool proxies through an internal route that resolves the Jira cloudId and the Assets workspaceId, then calls the Assets API via the OAuth 2.0 (3LO) gateway form (/ex/jira/{cloudId}/jsm/assets/workspace/{workspaceId}/v1). Adds the CMDB OAuth scopes to the jira provider (read/write/delete cmdb-object, read cmdb-schema/type/attribute) with descriptions, contract schemas for each route, and block operations/subBlocks/outputs. Bumps the API-validation route baseline for the nine new routes. * refactor(jsm): harden Assets param coercion and response typing - Add toOptionalInt helper so non-numeric pagination inputs never emit NaN into the Assets query string (startAt/maxResults/page/resultsPerPage) - Replace Record in mapAssetObject with typed Raw* interfaces * fix(jsm): validate Assets workspaceId and honor `last` pagination flag Address review findings on the Assets tools: - Add validateAssetsWorkspaceId and guard the workspaceId in every Assets route before it is interpolated into the API path (mirrors the existing cloudId guard) — prevents a crafted workspaceId from escaping the workspace-scoped path - Object schema list now falls back to the `last` flag when `isLast` is absent, so pagination doesn't stop early * feat(jsm): allow overriding the auto-resolved Assets workspace Atlassian provisions one Assets workspace per site, so workspace discovery uses values[0] by design. For the rare multi-workspace site, expose an advanced "Assets Workspace ID" override on the block that flows through to every Assets operation, and document the single-workspace assumption. * refactor(jsm): include Assets responses in the JsmResponse union Append the nine Assets tool response types to JsmResponse for completeness and consistency with the rest of the JSM tool surface. --- .../integrations/jira_service_management.mdx | 266 +++++++++++++ .../api/tools/jsm/assets/attributes/route.ts | 98 +++++ .../tools/jsm/assets/object-types/route.ts | 97 +++++ .../tools/jsm/assets/object/create/route.ts | 93 +++++ .../tools/jsm/assets/object/delete/route.ts | 81 ++++ .../api/tools/jsm/assets/object/get/route.ts | 88 +++++ .../tools/jsm/assets/object/update/route.ts | 97 +++++ .../app/api/tools/jsm/assets/schema/route.ts | 83 ++++ .../app/api/tools/jsm/assets/schemas/route.ts | 97 +++++ .../app/api/tools/jsm/assets/search/route.ts | 122 ++++++ .../blocks/blocks/jira_service_management.ts | 370 ++++++++++++++++++ apps/sim/lib/api/contracts/selectors/jsm.ts | 111 ++++++ .../sim/lib/core/security/input-validation.ts | 21 + apps/sim/lib/integrations/integrations.json | 38 +- apps/sim/lib/oauth/oauth.ts | 6 + apps/sim/lib/oauth/utils.ts | 6 + apps/sim/tools/jsm/create_object.ts | 97 +++++ apps/sim/tools/jsm/delete_object.ts | 84 ++++ apps/sim/tools/jsm/get_object.ts | 88 +++++ apps/sim/tools/jsm/get_object_schema.ts | 102 +++++ .../tools/jsm/get_object_type_attributes.ts | 136 +++++++ apps/sim/tools/jsm/index.ts | 18 + apps/sim/tools/jsm/list_object_schemas.ts | 126 ++++++ apps/sim/tools/jsm/list_object_types.ts | 117 ++++++ apps/sim/tools/jsm/search_objects_aql.ts | 150 +++++++ apps/sim/tools/jsm/types.ts | 226 +++++++++++ apps/sim/tools/jsm/update_object.ts | 104 +++++ apps/sim/tools/jsm/utils.ts | 104 +++++ apps/sim/tools/registry.ts | 18 + scripts/check-api-validation-contracts.ts | 4 +- 30 files changed, 3045 insertions(+), 3 deletions(-) create mode 100644 apps/sim/app/api/tools/jsm/assets/attributes/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/object-types/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/object/create/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/object/delete/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/object/get/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/object/update/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/schema/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/schemas/route.ts create mode 100644 apps/sim/app/api/tools/jsm/assets/search/route.ts create mode 100644 apps/sim/tools/jsm/create_object.ts create mode 100644 apps/sim/tools/jsm/delete_object.ts create mode 100644 apps/sim/tools/jsm/get_object.ts create mode 100644 apps/sim/tools/jsm/get_object_schema.ts create mode 100644 apps/sim/tools/jsm/get_object_type_attributes.ts create mode 100644 apps/sim/tools/jsm/list_object_schemas.ts create mode 100644 apps/sim/tools/jsm/list_object_types.ts create mode 100644 apps/sim/tools/jsm/search_objects_aql.ts create mode 100644 apps/sim/tools/jsm/update_object.ts diff --git a/apps/docs/content/docs/en/integrations/jira_service_management.mdx b/apps/docs/content/docs/en/integrations/jira_service_management.mdx index 46440071f19..b22676bbbc1 100644 --- a/apps/docs/content/docs/en/integrations/jira_service_management.mdx +++ b/apps/docs/content/docs/en/integrations/jira_service_management.mdx @@ -988,6 +988,272 @@ Copy forms from one Jira issue to another | `copiedForms` | json | Array of successfully copied forms | | `errors` | json | Array of errors encountered during copy | +### `jsm_list_object_schemas` + +List Assets (Insight/CMDB) object schemas in Jira Service Management + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `startAt` | number | No | Pagination start index \(e.g., 0, 50\) | +| `maxResults` | number | No | Maximum schemas to return \(e.g., 25, 50\) | +| `includeCounts` | boolean | No | Include object and object-type counts per schema | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `schemas` | array | List of Assets object schemas | +| ↳ `id` | string | Schema ID | +| ↳ `name` | string | Schema name | +| ↳ `objectSchemaKey` | string | Schema key | +| ↳ `status` | string | Schema status | +| ↳ `description` | string | Schema description | +| ↳ `objectCount` | number | Number of objects | +| ↳ `objectTypeCount` | number | Number of object types | +| `total` | number | Total number of schemas | +| `isLast` | boolean | Whether this is the last page | + +### `jsm_get_object_schema` + +Get a single Assets (Insight/CMDB) object schema by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `schemaId` | string | Yes | The Assets object schema ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `schema` | json | The Assets object schema | +| ↳ `id` | string | Schema ID | +| ↳ `name` | string | Schema name | +| ↳ `objectSchemaKey` | string | Schema key | +| ↳ `status` | string | Schema status | +| ↳ `description` | string | Schema description | +| ↳ `objectCount` | number | Number of objects | +| ↳ `objectTypeCount` | number | Number of object types | + +### `jsm_list_object_types` + +List object types within an Assets (Insight/CMDB) object schema + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `schemaId` | string | Yes | The Assets object schema ID to list object types for | +| `excludeAbstract` | boolean | No | Exclude abstract object types from the result | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `objectTypes` | array | List of object types in the schema | +| ↳ `id` | string | Object type ID | +| ↳ `name` | string | Object type name | +| ↳ `description` | string | Object type description | +| ↳ `objectSchemaId` | string | Parent schema ID | +| ↳ `objectCount` | number | Number of objects | +| ↳ `abstractObjectType` | boolean | Whether the type is abstract | +| ↳ `inherited` | boolean | Whether the type inherits attributes | +| `total` | number | Total number of object types | + +### `jsm_get_object_type_attributes` + +Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `objectTypeId` | string | Yes | The Assets object type ID | +| `onlyValueEditable` | boolean | No | Return only attributes whose values can be edited | +| `query` | string | No | Filter attributes by a search query | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `attributes` | array | Attribute definitions for the object type | +| ↳ `id` | string | Attribute definition ID — use as objectTypeAttributeId in create/update | +| ↳ `name` | string | Attribute name | +| ↳ `label` | boolean | Whether this attribute is the object label | +| ↳ `type` | number | Data type discriminator \(integer enum\) | +| ↳ `defaultType` | json | Default data type \{ id, name \} | +| ↳ `editable` | boolean | Whether the value is editable | +| ↳ `minimumCardinality` | number | Minimum number of values \(>= 1 means required\) | +| ↳ `maximumCardinality` | number | Maximum number of values | +| ↳ `uniqueAttribute` | boolean | Whether values must be unique | +| `total` | number | Total number of attributes | + +### `jsm_search_objects_aql` + +Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `qlQuery` | string | Yes | AQL query string \(e.g., objectType = "Host" AND "Operating System" = "Ubuntu"\) | +| `page` | number | No | Page number \(1-based, defaults to 1\) | +| `resultsPerPage` | number | No | Results per page \(e.g., 25, 50\) | +| `includeAttributes` | boolean | No | Include resolved attribute values on each object \(defaults to true\) | +| `objectTypeId` | string | No | Optionally scope the search to a single object type ID | +| `objectSchemaId` | string | No | Optionally scope the search to a single object schema ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `objects` | array | Matching Assets objects | +| ↳ `id` | string | Object ID | +| ↳ `label` | string | Object label | +| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) | +| ↳ `objectType` | json | Object type metadata | +| ↳ `attributes` | json | Resolved attribute values | +| `total` | number | Total number of matching objects \(totalFilterCount\) | +| `pageNumber` | number | Current page number | +| `pageSize` | number | Number of objects on this page | + +### `jsm_get_object` + +Get a single Assets (Insight/CMDB) object by ID, including its attribute values + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `objectId` | string | Yes | The Assets object ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `object` | json | The Assets object | +| ↳ `id` | string | Object ID | +| ↳ `label` | string | Human-readable object label | +| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) | +| ↳ `globalId` | string | Global object ID | +| ↳ `objectType` | json | Object type metadata | +| ↳ `attributes` | json | Resolved attribute values for the object | +| ↳ `hasAvatar` | boolean | Whether the object has an avatar | +| ↳ `created` | string | Creation timestamp | +| ↳ `updated` | string | Last update timestamp | +| ↳ `link` | string | Self link to the object | + +### `jsm_create_object` + +Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `objectTypeId` | string | Yes | The object type ID to create the object under | +| `attributes` | json | Yes | Array of attributes: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `object` | json | The created Assets object | +| ↳ `id` | string | Object ID | +| ↳ `label` | string | Human-readable object label | +| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) | +| ↳ `globalId` | string | Global object ID | +| ↳ `objectType` | json | Object type metadata | +| ↳ `attributes` | json | Resolved attribute values for the object | +| ↳ `hasAvatar` | boolean | Whether the object has an avatar | +| ↳ `created` | string | Creation timestamp | +| ↳ `updated` | string | Last update timestamp | +| ↳ `link` | string | Self link to the object | + +### `jsm_update_object` + +Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `objectId` | string | Yes | The Assets object ID to update | +| `attributes` | json | Yes | Array of attributes to set: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] | +| `objectTypeId` | string | No | Optional object type ID \(only if changing the type\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `object` | json | The updated Assets object | +| ↳ `id` | string | Object ID | +| ↳ `label` | string | Human-readable object label | +| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) | +| ↳ `globalId` | string | Global object ID | +| ↳ `objectType` | json | Object type metadata | +| ↳ `attributes` | json | Resolved attribute values for the object | +| ↳ `hasAvatar` | boolean | Whether the object has an avatar | +| ↳ `created` | string | Creation timestamp | +| ↳ `updated` | string | Last update timestamp | +| ↳ `link` | string | Self link to the object | + +### `jsm_delete_object` + +Delete an Assets (Insight/CMDB) object by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance | +| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) | +| `objectId` | string | Yes | The Assets object ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `objectId` | string | The deleted object ID | +| `deleted` | boolean | Whether the object was deleted | + ## Triggers diff --git a/apps/sim/app/api/tools/jsm/assets/attributes/route.ts b/apps/sim/app/api/tools/jsm/assets/attributes/route.ts new file mode 100644 index 00000000000..28804c427f2 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/attributes/route.ts @@ -0,0 +1,98 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsAttributesAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmObjectTypeAttributesContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + objectTypeId, + onlyValueEditable, + query: searchQuery, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const query = new URLSearchParams() + if (onlyValueEditable !== undefined) { + query.append('onlyValueEditable', String(onlyValueEditable)) + } + if (searchQuery) query.append('query', searchQuery) + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objecttype/${encodeURIComponent( + objectTypeId + )}/attributes${query.toString() ? `?${query.toString()}` : ''}` + + const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error getting attributes', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + const attributes = Array.isArray(data) ? data : (data.values ?? []) + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + attributes, + total: attributes.length, + }, + }) + } catch (error) { + logger.error('Error getting Assets attributes', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/object-types/route.ts b/apps/sim/app/api/tools/jsm/assets/object-types/route.ts new file mode 100644 index 00000000000..328ccaa8c69 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/object-types/route.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmListObjectTypesContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsObjectTypesAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmListObjectTypesContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + schemaId, + excludeAbstract, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const query = new URLSearchParams() + if (excludeAbstract !== undefined) query.append('excludeAbstract', String(excludeAbstract)) + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent( + schemaId + )}/objecttypes${query.toString() ? `?${query.toString()}` : ''}` + + const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error listing object types', { + status: response.status, + errorText, + }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + const objectTypes = Array.isArray(data) ? data : (data.values ?? []) + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + objectTypes, + total: objectTypes.length, + }, + }) + } catch (error) { + logger.error('Error listing Assets object types', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/object/create/route.ts b/apps/sim/app/api/tools/jsm/assets/object/create/route.ts new file mode 100644 index 00000000000..7c85f69a0fd --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/object/create/route.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmCreateObjectContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { + getAssetsApiBaseUrl, + getJsmHeaders, + mapAssetObject, + resolveAssetsContext, +} from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsCreateObjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmCreateObjectContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + objectTypeId, + attributes, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/create` + + const response = await fetch(url, { + method: 'POST', + headers: getJsmHeaders(accessToken), + body: JSON.stringify({ objectTypeId, attributes }), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error creating object', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { ts: new Date().toISOString(), object: mapAssetObject(data) }, + }) + } catch (error) { + logger.error('Error creating Assets object', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/object/delete/route.ts b/apps/sim/app/api/tools/jsm/assets/object/delete/route.ts new file mode 100644 index 00000000000..cf4dd4b1d4a --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/object/delete/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmDeleteObjectContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsDeleteObjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmDeleteObjectContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + objectId, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}` + + const response = await fetch(url, { method: 'DELETE', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error deleting object', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + return NextResponse.json({ + success: true, + output: { ts: new Date().toISOString(), objectId, deleted: true }, + }) + } catch (error) { + logger.error('Error deleting Assets object', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/object/get/route.ts b/apps/sim/app/api/tools/jsm/assets/object/get/route.ts new file mode 100644 index 00000000000..fa7fa3759e9 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/object/get/route.ts @@ -0,0 +1,88 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmGetObjectContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { + getAssetsApiBaseUrl, + getJsmHeaders, + mapAssetObject, + resolveAssetsContext, +} from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsGetObjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmGetObjectContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + objectId, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}` + + const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error getting object', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { ts: new Date().toISOString(), object: mapAssetObject(data) }, + }) + } catch (error) { + logger.error('Error getting Assets object', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/object/update/route.ts b/apps/sim/app/api/tools/jsm/assets/object/update/route.ts new file mode 100644 index 00000000000..bfc7a6286a8 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/object/update/route.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmUpdateObjectContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { + getAssetsApiBaseUrl, + getJsmHeaders, + mapAssetObject, + resolveAssetsContext, +} from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsUpdateObjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmUpdateObjectContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + objectId, + objectTypeId, + attributes, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}` + + const body: Record = { attributes } + if (objectTypeId) body.objectTypeId = objectTypeId + + const response = await fetch(url, { + method: 'PUT', + headers: getJsmHeaders(accessToken), + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error updating object', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { ts: new Date().toISOString(), object: mapAssetObject(data) }, + }) + } catch (error) { + logger.error('Error updating Assets object', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/schema/route.ts b/apps/sim/app/api/tools/jsm/assets/schema/route.ts new file mode 100644 index 00000000000..d2eb97d01c7 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/schema/route.ts @@ -0,0 +1,83 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmGetObjectSchemaContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsSchemaAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmGetObjectSchemaContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + schemaId, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(schemaId)}` + + const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error getting schema', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { ts: new Date().toISOString(), schema: data ?? null }, + }) + } catch (error) { + logger.error('Error getting Assets schema', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/schemas/route.ts b/apps/sim/app/api/tools/jsm/assets/schemas/route.ts new file mode 100644 index 00000000000..5219e97853d --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/schemas/route.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmListObjectSchemasContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsSchemasAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmListObjectSchemasContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + startAt, + maxResults, + includeCounts, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const query = new URLSearchParams() + if (startAt !== undefined) query.append('startAt', String(startAt)) + if (maxResults !== undefined) query.append('maxResults', String(maxResults)) + if (includeCounts !== undefined) query.append('includeCounts', String(includeCounts)) + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/list${ + query.toString() ? `?${query.toString()}` : '' + }` + + const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error listing schemas', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + schemas: data.values ?? [], + total: data.total ?? (data.values?.length || 0), + isLast: data.isLast ?? data.last ?? true, + }, + }) + } catch (error) { + logger.error('Error listing Assets schemas', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/jsm/assets/search/route.ts b/apps/sim/app/api/tools/jsm/assets/search/route.ts new file mode 100644 index 00000000000..3ddad2dae78 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/assets/search/route.ts @@ -0,0 +1,122 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { jsmSearchObjectsAqlContract } from '@/lib/api/contracts/selectors/jsm' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + validateAssetsWorkspaceId, + validateJiraCloudId, +} from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { + getAssetsApiBaseUrl, + getJsmHeaders, + mapAssetObject, + resolveAssetsContext, +} from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmAssetsSearchAPI') + +/** Coerce a string|number|boolean param into a number, falling back when unset */ +function toNumber(value: string | number | undefined, fallback: number): number { + if (value === undefined) return fallback + const parsed = typeof value === 'number' ? value : Number(value) + return Number.isFinite(parsed) ? parsed : fallback +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest(jsmSearchObjectsAqlContract, request, {}) + if (!parsed.success) return parsed.response + + const { + domain, + accessToken, + cloudId: cloudIdParam, + workspaceId: workspaceIdParam, + qlQuery, + page, + resultsPerPage, + includeAttributes, + objectTypeId, + objectSchemaId, + } = parsed.data.body + + const { cloudId, workspaceId } = await resolveAssetsContext( + domain, + accessToken, + cloudIdParam, + workspaceIdParam + ) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId') + if (!workspaceIdValidation.isValid) { + return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 }) + } + + const includeAttrs = + includeAttributes === undefined ? true : String(includeAttributes) === 'true' + + const body: Record = { + qlQuery, + page: toNumber(page, 1), + resultsPerPage: toNumber(resultsPerPage, 25), + includeAttributes: includeAttrs, + } + if (objectTypeId) body.objectTypeId = objectTypeId + if (objectSchemaId) body.objectSchemaId = objectSchemaId + + const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/aql` + + const response = await fetch(url, { + method: 'POST', + headers: getJsmHeaders(accessToken), + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Assets API error running AQL search', { status: response.status, errorText }) + return NextResponse.json( + { + error: parseAtlassianErrorMessage(response.status, response.statusText, errorText), + details: errorText, + }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + objects: Array.isArray(data.objectEntries) ? data.objectEntries.map(mapAssetObject) : [], + total: data.totalFilterCount ?? (data.objectEntries?.length || 0), + pageNumber: data.pageNumber ?? 1, + pageSize: data.pageSize ?? (data.objectEntries?.length || 0), + }, + }) + } catch (error) { + logger.error('Error running Assets AQL search', { error: toError(error).message }) + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index cad695a1832..a32841ec48c 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -5,6 +5,38 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import type { JsmResponse } from '@/tools/jsm/types' import { getTrigger } from '@/triggers' +/** + * Coerce an optional numeric block input into an integer, returning undefined for + * empty or non-numeric values so no `NaN` reaches the API query string. + */ +function toOptionalInt(value: string | undefined): number | undefined { + if (!value) return undefined + const parsed = Number.parseInt(value, 10) + return Number.isNaN(parsed) ? undefined : parsed +} + +/** + * Parse the Assets attributes input into the API payload array. Accepts either a + * JSON string (from the block input) or an already-parsed array (from a dynamic + * reference). Throws a clear error when the value is not a valid array. + */ +function parseAssetAttributes(value: unknown): unknown[] { + if (Array.isArray(value)) return value + if (typeof value === 'string') { + let parsed: unknown + try { + parsed = JSON.parse(value) + } catch { + throw new Error('Attributes must be a valid JSON array') + } + if (!Array.isArray(parsed)) { + throw new Error('Attributes must be a JSON array') + } + return parsed + } + throw new Error('Attributes are required') +} + export const JiraServiceManagementBlock: BlockConfig = { type: 'jira_service_management', name: 'Jira Service Management', @@ -57,6 +89,15 @@ export const JiraServiceManagementBlock: BlockConfig = { { label: 'Externalise Form', id: 'externalise_form' }, { label: 'Internalise Form', id: 'internalise_form' }, { label: 'Copy Forms', id: 'copy_forms' }, + { label: 'List Asset Schemas', id: 'list_object_schemas' }, + { label: 'Get Asset Schema', id: 'get_object_schema' }, + { label: 'List Asset Object Types', id: 'list_object_types' }, + { label: 'Get Asset Object Type Attributes', id: 'get_object_type_attributes' }, + { label: 'Search Assets (AQL)', id: 'search_objects_aql' }, + { label: 'Get Asset Object', id: 'get_object' }, + { label: 'Create Asset Object', id: 'create_object' }, + { label: 'Update Asset Object', id: 'update_object' }, + { label: 'Delete Asset Object', id: 'delete_object' }, ], value: () => 'get_service_desks', }, @@ -564,6 +605,185 @@ Return ONLY the comment text - no explanations.`, ], }, }, + { + id: 'assetSchemaId', + title: 'Schema ID', + type: 'short-input', + placeholder: 'e.g., 1', + required: { field: 'operation', value: ['get_object_schema', 'list_object_types'] }, + condition: { field: 'operation', value: ['get_object_schema', 'list_object_types'] }, + }, + { + id: 'assetObjectTypeId', + title: 'Object Type ID', + type: 'short-input', + placeholder: 'e.g., 23', + required: { field: 'operation', value: ['get_object_type_attributes', 'create_object'] }, + condition: { + field: 'operation', + value: [ + 'get_object_type_attributes', + 'create_object', + 'update_object', + 'search_objects_aql', + ], + }, + }, + { + id: 'assetObjectId', + title: 'Object ID', + type: 'short-input', + placeholder: 'e.g., 1234', + required: { field: 'operation', value: ['get_object', 'update_object', 'delete_object'] }, + condition: { field: 'operation', value: ['get_object', 'update_object', 'delete_object'] }, + }, + { + id: 'assetQlQuery', + title: 'AQL Query', + type: 'long-input', + placeholder: 'objectType = "Host" AND "Operating System" = "Ubuntu"', + required: { field: 'operation', value: 'search_objects_aql' }, + condition: { field: 'operation', value: 'search_objects_aql' }, + wandConfig: { + enabled: true, + placeholder: 'Describe which assets to find', + prompt: + 'Generate an Atlassian Assets AQL (Assets Query Language) query for the user request. Use attribute = "value" comparisons, AND/OR, IN, LIKE, and objectType filters. Example: objectType = "Host" AND Status = "Running". Return ONLY the AQL query - no explanations, no extra text.', + }, + }, + { + id: 'assetAttributes', + title: 'Attributes', + type: 'long-input', + placeholder: + '[{ "objectTypeAttributeId": "135", "objectAttributeValues": [{ "value": "Server-1" }] }]', + required: { field: 'operation', value: ['create_object', 'update_object'] }, + condition: { field: 'operation', value: ['create_object', 'update_object'] }, + wandConfig: { + enabled: true, + generationType: 'json-object', + placeholder: 'Describe the attribute values to set', + prompt: + 'Generate a JSON array of Atlassian Assets object attributes. Each element is { "objectTypeAttributeId": "", "objectAttributeValues": [{ "value": "" }] }. Use objectTypeAttributeId values from the object type attribute definitions. Return ONLY the JSON array - no explanations, no extra text.', + }, + }, + { + id: 'assetStartAt', + title: 'Start At', + type: 'short-input', + placeholder: 'Pagination start index (default: 0)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_object_schemas' }, + }, + { + id: 'assetMaxResults', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum schemas to return (default: 25)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_object_schemas' }, + }, + { + id: 'assetIncludeCounts', + title: 'Include Counts', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'list_object_schemas' }, + }, + { + id: 'assetExcludeAbstract', + title: 'Exclude Abstract Types', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'list_object_types' }, + }, + { + id: 'assetOnlyValueEditable', + title: 'Only Editable Attributes', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'get_object_type_attributes' }, + }, + { + id: 'assetAttributeQuery', + title: 'Attribute Filter', + type: 'short-input', + placeholder: 'Filter attributes by name', + mode: 'advanced', + condition: { field: 'operation', value: 'get_object_type_attributes' }, + }, + { + id: 'assetPage', + title: 'Page', + type: 'short-input', + placeholder: 'Page number (default: 1)', + mode: 'advanced', + condition: { field: 'operation', value: 'search_objects_aql' }, + }, + { + id: 'assetResultsPerPage', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'Results per page (default: 25)', + mode: 'advanced', + condition: { field: 'operation', value: 'search_objects_aql' }, + }, + { + id: 'assetIncludeAttributes', + title: 'Include Attributes', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + mode: 'advanced', + condition: { field: 'operation', value: 'search_objects_aql' }, + }, + { + id: 'assetObjectSchemaId', + title: 'Object Schema ID', + type: 'short-input', + placeholder: 'Scope the search to a schema ID', + mode: 'advanced', + condition: { field: 'operation', value: 'search_objects_aql' }, + }, + { + id: 'assetWorkspaceId', + title: 'Assets Workspace ID', + type: 'short-input', + placeholder: 'Override the auto-resolved Assets workspace', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_object_schemas', + 'get_object_schema', + 'list_object_types', + 'get_object_type_attributes', + 'search_objects_aql', + 'get_object', + 'create_object', + 'update_object', + 'delete_object', + ], + }, + }, ...getTrigger('jsm_request_created').subBlocks, ...getTrigger('jsm_request_updated').subBlocks, ...getTrigger('jsm_request_commented').subBlocks, @@ -606,6 +826,15 @@ Return ONLY the comment text - no explanations.`, 'jsm_externalise_form', 'jsm_internalise_form', 'jsm_copy_forms', + 'jsm_list_object_schemas', + 'jsm_get_object_schema', + 'jsm_list_object_types', + 'jsm_get_object_type_attributes', + 'jsm_search_objects_aql', + 'jsm_get_object', + 'jsm_create_object', + 'jsm_update_object', + 'jsm_delete_object', ], config: { tool: (params) => { @@ -678,6 +907,24 @@ Return ONLY the comment text - no explanations.`, return 'jsm_internalise_form' case 'copy_forms': return 'jsm_copy_forms' + case 'list_object_schemas': + return 'jsm_list_object_schemas' + case 'get_object_schema': + return 'jsm_get_object_schema' + case 'list_object_types': + return 'jsm_list_object_types' + case 'get_object_type_attributes': + return 'jsm_get_object_type_attributes' + case 'search_objects_aql': + return 'jsm_search_objects_aql' + case 'get_object': + return 'jsm_get_object' + case 'create_object': + return 'jsm_create_object' + case 'update_object': + return 'jsm_update_object' + case 'delete_object': + return 'jsm_delete_object' default: return 'jsm_get_service_desks' } @@ -688,6 +935,13 @@ Return ONLY the comment text - no explanations.`, domain: params.domain, } + // Assets tools accept an optional workspaceId override; when omitted the + // route resolves the site's single Assets workspace automatically. + const assetBaseParams = { + ...baseParams, + workspaceId: params.assetWorkspaceId || undefined, + } + switch (params.operation) { case 'get_service_desks': return { @@ -1109,6 +1363,79 @@ Return ONLY the comment text - no explanations.`, })() : undefined, } + case 'list_object_schemas': + return { + ...assetBaseParams, + startAt: toOptionalInt(params.assetStartAt), + maxResults: toOptionalInt(params.assetMaxResults), + includeCounts: params.assetIncludeCounts === 'true' ? true : undefined, + } + case 'get_object_schema': + if (!params.assetSchemaId) { + throw new Error('Schema ID is required') + } + return { ...assetBaseParams, schemaId: params.assetSchemaId } + case 'list_object_types': + if (!params.assetSchemaId) { + throw new Error('Schema ID is required') + } + return { + ...assetBaseParams, + schemaId: params.assetSchemaId, + excludeAbstract: params.assetExcludeAbstract === 'true' ? true : undefined, + } + case 'get_object_type_attributes': + if (!params.assetObjectTypeId) { + throw new Error('Object type ID is required') + } + return { + ...assetBaseParams, + objectTypeId: params.assetObjectTypeId, + onlyValueEditable: params.assetOnlyValueEditable === 'true' ? true : undefined, + query: params.assetAttributeQuery || undefined, + } + case 'search_objects_aql': + if (!params.assetQlQuery) { + throw new Error('AQL query is required') + } + return { + ...assetBaseParams, + qlQuery: params.assetQlQuery, + page: toOptionalInt(params.assetPage), + resultsPerPage: toOptionalInt(params.assetResultsPerPage), + includeAttributes: params.assetIncludeAttributes === 'false' ? false : undefined, + objectTypeId: params.assetObjectTypeId || undefined, + objectSchemaId: params.assetObjectSchemaId || undefined, + } + case 'get_object': + if (!params.assetObjectId) { + throw new Error('Object ID is required') + } + return { ...assetBaseParams, objectId: params.assetObjectId } + case 'create_object': + if (!params.assetObjectTypeId) { + throw new Error('Object type ID is required') + } + return { + ...assetBaseParams, + objectTypeId: params.assetObjectTypeId, + attributes: parseAssetAttributes(params.assetAttributes), + } + case 'update_object': + if (!params.assetObjectId) { + throw new Error('Object ID is required') + } + return { + ...assetBaseParams, + objectId: params.assetObjectId, + attributes: parseAssetAttributes(params.assetAttributes), + objectTypeId: params.assetObjectTypeId || undefined, + } + case 'delete_object': + if (!params.assetObjectId) { + throw new Error('Object ID is required') + } + return { ...assetBaseParams, objectId: params.assetObjectId } default: return baseParams } @@ -1167,6 +1494,25 @@ Return ONLY the comment text - no explanations.`, searchQuery: { type: 'string', description: 'Filter request types by name' }, groupId: { type: 'string', description: 'Filter by request type group ID' }, expand: { type: 'string', description: 'Comma-separated fields to expand' }, + assetSchemaId: { type: 'string', description: 'Assets object schema ID' }, + assetObjectTypeId: { type: 'string', description: 'Assets object type ID' }, + assetObjectId: { type: 'string', description: 'Assets object ID' }, + assetQlQuery: { type: 'string', description: 'AQL query string' }, + assetAttributes: { type: 'string', description: 'JSON array of Assets object attributes' }, + assetStartAt: { type: 'string', description: 'Schema pagination start index' }, + assetMaxResults: { type: 'string', description: 'Maximum schemas to return' }, + assetIncludeCounts: { type: 'string', description: 'Include object/type counts per schema' }, + assetExcludeAbstract: { type: 'string', description: 'Exclude abstract object types' }, + assetOnlyValueEditable: { type: 'string', description: 'Return only editable attributes' }, + assetAttributeQuery: { type: 'string', description: 'Filter attributes by name' }, + assetPage: { type: 'string', description: 'AQL search page number' }, + assetResultsPerPage: { type: 'string', description: 'AQL search results per page' }, + assetIncludeAttributes: { type: 'string', description: 'Include attribute values in results' }, + assetObjectSchemaId: { type: 'string', description: 'Scope AQL search to a schema ID' }, + assetWorkspaceId: { + type: 'string', + description: 'Override the auto-resolved Assets workspace ID', + }, }, outputs: { ts: { type: 'string', description: 'Timestamp of the operation' }, @@ -1250,6 +1596,30 @@ Return ONLY the comment text - no explanations.`, errors: { type: 'json', description: 'Array of errors from copy forms operation' }, sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' }, targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' }, + schemas: { + type: 'json', + description: 'Array of Assets object schemas (id, name, objectSchemaKey, status)', + }, + schema: { type: 'json', description: 'Single Assets object schema' }, + objectTypes: { + type: 'json', + description: 'Array of Assets object types (id, name, objectSchemaId, objectCount)', + }, + attributes: { + type: 'json', + description: + 'Array of object type attribute definitions (id, name, type, minimumCardinality)', + }, + objects: { + type: 'json', + description: 'Array of Assets objects from an AQL search (id, label, objectKey, attributes)', + }, + object: { + type: 'json', + description: 'Single Assets object (id, label, objectKey, objectType, attributes)', + }, + objectId: { type: 'string', description: 'Assets object ID (delete operation)' }, + isLast: { type: 'boolean', description: 'Whether this is the last page of schemas' }, }, triggers: { enabled: true, diff --git a/apps/sim/lib/api/contracts/selectors/jsm.ts b/apps/sim/lib/api/contracts/selectors/jsm.ts index f8fc3c40e93..dea80b9344c 100644 --- a/apps/sim/lib/api/contracts/selectors/jsm.ts +++ b/apps/sim/lib/api/contracts/selectors/jsm.ts @@ -185,6 +185,80 @@ export const jsmCopyFormsBodySchema = jsmBaseBodySchema.extend({ formIds: z.array(z.string(), { error: 'formIds must be an array of form UUIDs' }).optional(), }) +const jsmAssetsBaseBodySchema = jsmBaseBodySchema.extend({ + workspaceId: z.string().optional(), +}) + +const jsmAssetsPaginationField = z.union([z.string(), z.number()]).optional() + +export const jsmListObjectSchemasBodySchema = jsmAssetsBaseBodySchema.extend({ + startAt: jsmAssetsPaginationField, + maxResults: jsmAssetsPaginationField, + includeCounts: z.union([z.string(), z.boolean()]).optional(), +}) + +export const jsmObjectSchemaBodySchema = jsmAssetsBaseBodySchema.extend({ + schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'), +}) + +export const jsmObjectTypesBodySchema = jsmAssetsBaseBodySchema.extend({ + schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'), + excludeAbstract: z.union([z.string(), z.boolean()]).optional(), +}) + +export const jsmObjectTypeAttributesBodySchema = jsmAssetsBaseBodySchema.extend({ + objectTypeId: z + .string({ error: 'Object type ID is required' }) + .min(1, 'Object type ID is required'), + onlyValueEditable: z.union([z.string(), z.boolean()]).optional(), + query: z.string().optional(), +}) + +export const jsmSearchObjectsAqlBodySchema = jsmAssetsBaseBodySchema.extend({ + qlQuery: z.string({ error: 'AQL query is required' }).min(1, 'AQL query is required'), + page: jsmAssetsPaginationField, + resultsPerPage: jsmAssetsPaginationField, + includeAttributes: z.union([z.string(), z.boolean()]).optional(), + objectTypeId: z.string().optional(), + objectSchemaId: z.string().optional(), +}) + +export const jsmGetObjectBodySchema = jsmAssetsBaseBodySchema.extend({ + objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'), +}) + +const jsmAssetAttributeInputSchema = z.object({ + objectTypeAttributeId: z + .string({ error: 'objectTypeAttributeId is required' }) + .min(1, 'objectTypeAttributeId is required'), + objectAttributeValues: z + .array(z.object({ value: z.unknown() }), { + error: 'objectAttributeValues must be an array of { value } entries', + }) + .min(1, 'Each attribute needs at least one value'), +}) + +export const jsmCreateObjectBodySchema = jsmAssetsBaseBodySchema.extend({ + objectTypeId: z + .string({ error: 'Object type ID is required' }) + .min(1, 'Object type ID is required'), + attributes: z + .array(jsmAssetAttributeInputSchema, { error: 'attributes is required' }) + .min(1, 'At least one attribute is required'), +}) + +export const jsmUpdateObjectBodySchema = jsmAssetsBaseBodySchema.extend({ + objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'), + objectTypeId: z.string().optional(), + attributes: z + .array(jsmAssetAttributeInputSchema, { error: 'attributes is required' }) + .min(1, 'At least one attribute is required'), +}) + +export const jsmDeleteObjectBodySchema = jsmAssetsBaseBodySchema.extend({ + objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'), +}) + export const defineJsmToolContract = (path: string, body: TBody) => definePostSelector(path, body, z.unknown()) @@ -314,6 +388,43 @@ export const jsmCopyFormsContract = defineJsmToolContract( jsmCopyFormsBodySchema ) +export const jsmListObjectSchemasContract = defineJsmToolContract( + '/api/tools/jsm/assets/schemas', + jsmListObjectSchemasBodySchema +) +export const jsmGetObjectSchemaContract = defineJsmToolContract( + '/api/tools/jsm/assets/schema', + jsmObjectSchemaBodySchema +) +export const jsmListObjectTypesContract = defineJsmToolContract( + '/api/tools/jsm/assets/object-types', + jsmObjectTypesBodySchema +) +export const jsmObjectTypeAttributesContract = defineJsmToolContract( + '/api/tools/jsm/assets/attributes', + jsmObjectTypeAttributesBodySchema +) +export const jsmSearchObjectsAqlContract = defineJsmToolContract( + '/api/tools/jsm/assets/search', + jsmSearchObjectsAqlBodySchema +) +export const jsmGetObjectContract = defineJsmToolContract( + '/api/tools/jsm/assets/object/get', + jsmGetObjectBodySchema +) +export const jsmCreateObjectContract = defineJsmToolContract( + '/api/tools/jsm/assets/object/create', + jsmCreateObjectBodySchema +) +export const jsmUpdateObjectContract = defineJsmToolContract( + '/api/tools/jsm/assets/object/update', + jsmUpdateObjectBodySchema +) +export const jsmDeleteObjectContract = defineJsmToolContract( + '/api/tools/jsm/assets/object/delete', + jsmDeleteObjectBodySchema +) + export type JsmServiceDesksBody = ContractBody export type JsmQueuesBody = ContractBody export type JsmRequestTypesBody = ContractBody diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 98ac9e1c982..57ba5939b98 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -627,6 +627,27 @@ export function validateJiraCloudId( }) } +/** + * Validates an Atlassian Assets workspace ID (a UUID-shaped, hyphenated + * alphanumeric identifier) before it is interpolated into an API path. + * + * @param value - The Assets workspace ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + */ +export function validateAssetsWorkspaceId( + value: string | null | undefined, + paramName = 'workspaceId' +): ValidationResult { + return validatePathSegment(value, { + paramName, + allowHyphens: true, + allowUnderscores: false, + allowDots: false, + maxLength: 100, + }) +} + /** * Validates Jira issue keys (format: PROJECT-123 or PROJECT-KEY-123) * diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 64e1d3ebc1c..5f11ba3c480 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -8445,9 +8445,45 @@ { "name": "Copy Forms", "description": "Copy forms from one Jira issue to another" + }, + { + "name": "List Asset Schemas", + "description": "List Assets (Insight/CMDB) object schemas in Jira Service Management" + }, + { + "name": "Get Asset Schema", + "description": "Get a single Assets (Insight/CMDB) object schema by ID" + }, + { + "name": "List Asset Object Types", + "description": "List object types within an Assets (Insight/CMDB) object schema" + }, + { + "name": "Get Asset Object Type Attributes", + "description": "Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns." + }, + { + "name": "Search Assets (AQL)", + "description": "Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = " + }, + { + "name": "Get Asset Object", + "description": "Get a single Assets (Insight/CMDB) object by ID, including its attribute values" + }, + { + "name": "Create Asset Object", + "description": "Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition." + }, + { + "name": "Update Asset Object", + "description": "Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values." + }, + { + "name": "Delete Asset Object", + "description": "Delete an Assets (Insight/CMDB) object by ID" } ], - "operationCount": 34, + "operationCount": 43, "triggers": [ { "id": "jsm_request_created", diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index dc8eed7cc8c..a9a71270042 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -547,6 +547,12 @@ export const OAUTH_PROVIDERS: Record = { 'write:request.participant:jira-service-management', 'read:request.approval:jira-service-management', 'write:request.approval:jira-service-management', + 'read:cmdb-object:jira', + 'write:cmdb-object:jira', + 'delete:cmdb-object:jira', + 'read:cmdb-schema:jira', + 'read:cmdb-type:jira', + 'read:cmdb-attribute:jira', ], }, }, diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index d9c4d76a61e..28276bc28fd 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -204,6 +204,12 @@ export const SCOPE_DESCRIPTIONS: Record = { 'Add and remove participants from customer requests', 'read:request.approval:jira-service-management': 'View approvals on customer requests', 'write:request.approval:jira-service-management': 'Approve or decline customer requests', + 'read:cmdb-object:jira': 'View Assets objects and run AQL searches', + 'write:cmdb-object:jira': 'Create and update Assets objects', + 'delete:cmdb-object:jira': 'Delete Assets objects', + 'read:cmdb-schema:jira': 'View Assets object schemas', + 'read:cmdb-type:jira': 'View Assets object types', + 'read:cmdb-attribute:jira': 'View Assets object type attributes', // Microsoft scopes 'User.Read': 'Read Microsoft user', diff --git a/apps/sim/tools/jsm/create_object.ts b/apps/sim/tools/jsm/create_object.ts new file mode 100644 index 00000000000..74c1dd497f0 --- /dev/null +++ b/apps/sim/tools/jsm/create_object.ts @@ -0,0 +1,97 @@ +import type { JsmCreateObjectParams, JsmCreateObjectResponse } from '@/tools/jsm/types' +import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmCreateObjectTool: ToolConfig = { + id: 'jsm_create_object', + name: 'JSM Create Asset Object', + description: + 'Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + objectTypeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object type ID to create the object under', + }, + attributes: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of attributes: [{ objectTypeAttributeId, objectAttributeValues: [{ value }] }]', + }, + }, + + request: { + url: '/api/tools/jsm/assets/object/create', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + objectTypeId: params.objectTypeId?.trim(), + attributes: params.attributes, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), object: null }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), object: null }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + object: { + type: 'json', + description: 'The created Assets object', + properties: ASSET_OBJECT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/jsm/delete_object.ts b/apps/sim/tools/jsm/delete_object.ts new file mode 100644 index 00000000000..0568a016d00 --- /dev/null +++ b/apps/sim/tools/jsm/delete_object.ts @@ -0,0 +1,84 @@ +import type { JsmDeleteObjectParams, JsmDeleteObjectResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmDeleteObjectTool: ToolConfig = { + id: 'jsm_delete_object', + name: 'JSM Delete Asset Object', + description: 'Delete an Assets (Insight/CMDB) object by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object ID to delete', + }, + }, + + request: { + url: '/api/tools/jsm/assets/object/delete', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + objectId: params.objectId?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), objectId: '', deleted: false }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), objectId: '', deleted: false }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + objectId: { type: 'string', description: 'The deleted object ID' }, + deleted: { type: 'boolean', description: 'Whether the object was deleted' }, + }, +} diff --git a/apps/sim/tools/jsm/get_object.ts b/apps/sim/tools/jsm/get_object.ts new file mode 100644 index 00000000000..182e509cd5b --- /dev/null +++ b/apps/sim/tools/jsm/get_object.ts @@ -0,0 +1,88 @@ +import type { JsmGetObjectParams, JsmGetObjectResponse } from '@/tools/jsm/types' +import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetObjectTool: ToolConfig = { + id: 'jsm_get_object', + name: 'JSM Get Asset Object', + description: 'Get a single Assets (Insight/CMDB) object by ID, including its attribute values', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object ID', + }, + }, + + request: { + url: '/api/tools/jsm/assets/object/get', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + objectId: params.objectId?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), object: null }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), object: null }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + object: { + type: 'json', + description: 'The Assets object', + properties: ASSET_OBJECT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/jsm/get_object_schema.ts b/apps/sim/tools/jsm/get_object_schema.ts new file mode 100644 index 00000000000..62906cf00db --- /dev/null +++ b/apps/sim/tools/jsm/get_object_schema.ts @@ -0,0 +1,102 @@ +import type { JsmGetObjectSchemaParams, JsmGetObjectSchemaResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetObjectSchemaTool: ToolConfig< + JsmGetObjectSchemaParams, + JsmGetObjectSchemaResponse +> = { + id: 'jsm_get_object_schema', + name: 'JSM Get Asset Schema', + description: 'Get a single Assets (Insight/CMDB) object schema by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + schemaId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object schema ID', + }, + }, + + request: { + url: '/api/tools/jsm/assets/schema', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + schemaId: params.schemaId?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), schema: null }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), schema: null }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + schema: { + type: 'json', + description: 'The Assets object schema', + properties: { + id: { type: 'string', description: 'Schema ID' }, + name: { type: 'string', description: 'Schema name' }, + objectSchemaKey: { type: 'string', description: 'Schema key' }, + status: { type: 'string', description: 'Schema status' }, + description: { type: 'string', description: 'Schema description', optional: true }, + objectCount: { type: 'number', description: 'Number of objects', optional: true }, + objectTypeCount: { + type: 'number', + description: 'Number of object types', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/jsm/get_object_type_attributes.ts b/apps/sim/tools/jsm/get_object_type_attributes.ts new file mode 100644 index 00000000000..5a58bd71bf3 --- /dev/null +++ b/apps/sim/tools/jsm/get_object_type_attributes.ts @@ -0,0 +1,136 @@ +import type { + JsmGetObjectTypeAttributesParams, + JsmGetObjectTypeAttributesResponse, +} from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetObjectTypeAttributesTool: ToolConfig< + JsmGetObjectTypeAttributesParams, + JsmGetObjectTypeAttributesResponse +> = { + id: 'jsm_get_object_type_attributes', + name: 'JSM Get Asset Object Type Attributes', + description: + 'Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + objectTypeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object type ID', + }, + onlyValueEditable: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return only attributes whose values can be edited', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter attributes by a search query', + }, + }, + + request: { + url: '/api/tools/jsm/assets/attributes', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + objectTypeId: params.objectTypeId?.trim(), + onlyValueEditable: params.onlyValueEditable, + query: params.query, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), attributes: [], total: 0 }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), attributes: [], total: 0 }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + attributes: { + type: 'array', + description: 'Attribute definitions for the object type', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Attribute definition ID — use as objectTypeAttributeId in create/update', + }, + name: { type: 'string', description: 'Attribute name' }, + label: { type: 'boolean', description: 'Whether this attribute is the object label' }, + type: { type: 'number', description: 'Data type discriminator (integer enum)' }, + defaultType: { + type: 'json', + description: 'Default data type { id, name }', + optional: true, + }, + editable: { type: 'boolean', description: 'Whether the value is editable' }, + minimumCardinality: { + type: 'number', + description: 'Minimum number of values (>= 1 means required)', + }, + maximumCardinality: { type: 'number', description: 'Maximum number of values' }, + uniqueAttribute: { + type: 'boolean', + description: 'Whether values must be unique', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Total number of attributes' }, + }, +} diff --git a/apps/sim/tools/jsm/index.ts b/apps/sim/tools/jsm/index.ts index e11696f0f3a..6f4fe50bc0b 100644 --- a/apps/sim/tools/jsm/index.ts +++ b/apps/sim/tools/jsm/index.ts @@ -5,9 +5,11 @@ import { jsmAddParticipantsTool } from '@/tools/jsm/add_participants' import { jsmAnswerApprovalTool } from '@/tools/jsm/answer_approval' import { jsmAttachFormTool } from '@/tools/jsm/attach_form' import { jsmCopyFormsTool } from '@/tools/jsm/copy_forms' +import { jsmCreateObjectTool } from '@/tools/jsm/create_object' import { jsmCreateOrganizationTool } from '@/tools/jsm/create_organization' import { jsmCreateRequestTool } from '@/tools/jsm/create_request' import { jsmDeleteFormTool } from '@/tools/jsm/delete_form' +import { jsmDeleteObjectTool } from '@/tools/jsm/delete_object' import { jsmExternaliseFormTool } from '@/tools/jsm/externalise_form' import { jsmGetApprovalsTool } from '@/tools/jsm/get_approvals' import { jsmGetCommentsTool } from '@/tools/jsm/get_comments' @@ -17,6 +19,9 @@ import { jsmGetFormAnswersTool } from '@/tools/jsm/get_form_answers' import { jsmGetFormStructureTool } from '@/tools/jsm/get_form_structure' import { jsmGetFormTemplatesTool } from '@/tools/jsm/get_form_templates' import { jsmGetIssueFormsTool } from '@/tools/jsm/get_issue_forms' +import { jsmGetObjectTool } from '@/tools/jsm/get_object' +import { jsmGetObjectSchemaTool } from '@/tools/jsm/get_object_schema' +import { jsmGetObjectTypeAttributesTool } from '@/tools/jsm/get_object_type_attributes' import { jsmGetOrganizationsTool } from '@/tools/jsm/get_organizations' import { jsmGetParticipantsTool } from '@/tools/jsm/get_participants' import { jsmGetQueuesTool } from '@/tools/jsm/get_queues' @@ -28,10 +33,14 @@ import { jsmGetServiceDesksTool } from '@/tools/jsm/get_service_desks' import { jsmGetSlaTool } from '@/tools/jsm/get_sla' import { jsmGetTransitionsTool } from '@/tools/jsm/get_transitions' import { jsmInternaliseFormTool } from '@/tools/jsm/internalise_form' +import { jsmListObjectSchemasTool } from '@/tools/jsm/list_object_schemas' +import { jsmListObjectTypesTool } from '@/tools/jsm/list_object_types' import { jsmReopenFormTool } from '@/tools/jsm/reopen_form' import { jsmSaveFormAnswersTool } from '@/tools/jsm/save_form_answers' +import { jsmSearchObjectsAqlTool } from '@/tools/jsm/search_objects_aql' import { jsmSubmitFormTool } from '@/tools/jsm/submit_form' import { jsmTransitionRequestTool } from '@/tools/jsm/transition_request' +import { jsmUpdateObjectTool } from '@/tools/jsm/update_object' export { jsmAddCommentTool, @@ -41,9 +50,11 @@ export { jsmAnswerApprovalTool, jsmAttachFormTool, jsmCopyFormsTool, + jsmCreateObjectTool, jsmCreateOrganizationTool, jsmCreateRequestTool, jsmDeleteFormTool, + jsmDeleteObjectTool, jsmExternaliseFormTool, jsmGetApprovalsTool, jsmGetCommentsTool, @@ -53,6 +64,9 @@ export { jsmGetFormStructureTool, jsmGetFormTemplatesTool, jsmGetIssueFormsTool, + jsmGetObjectTool, + jsmGetObjectSchemaTool, + jsmGetObjectTypeAttributesTool, jsmGetOrganizationsTool, jsmGetParticipantsTool, jsmGetQueuesTool, @@ -64,8 +78,12 @@ export { jsmGetSlaTool, jsmGetTransitionsTool, jsmInternaliseFormTool, + jsmListObjectSchemasTool, + jsmListObjectTypesTool, jsmReopenFormTool, jsmSaveFormAnswersTool, + jsmSearchObjectsAqlTool, jsmSubmitFormTool, jsmTransitionRequestTool, + jsmUpdateObjectTool, } diff --git a/apps/sim/tools/jsm/list_object_schemas.ts b/apps/sim/tools/jsm/list_object_schemas.ts new file mode 100644 index 00000000000..d08fbdb8527 --- /dev/null +++ b/apps/sim/tools/jsm/list_object_schemas.ts @@ -0,0 +1,126 @@ +import type { JsmListObjectSchemasParams, JsmListObjectSchemasResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmListObjectSchemasTool: ToolConfig< + JsmListObjectSchemasParams, + JsmListObjectSchemasResponse +> = { + id: 'jsm_list_object_schemas', + name: 'JSM List Asset Schemas', + description: 'List Assets (Insight/CMDB) object schemas in Jira Service Management', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + startAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pagination start index (e.g., 0, 50)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum schemas to return (e.g., 25, 50)', + }, + includeCounts: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include object and object-type counts per schema', + }, + }, + + request: { + url: '/api/tools/jsm/assets/schemas', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + startAt: params.startAt, + maxResults: params.maxResults, + includeCounts: params.includeCounts, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), schemas: [], total: 0, isLast: true }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { + ts: new Date().toISOString(), + schemas: [], + total: 0, + isLast: true, + }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + schemas: { + type: 'array', + description: 'List of Assets object schemas', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Schema ID' }, + name: { type: 'string', description: 'Schema name' }, + objectSchemaKey: { type: 'string', description: 'Schema key' }, + status: { type: 'string', description: 'Schema status' }, + description: { type: 'string', description: 'Schema description', optional: true }, + objectCount: { type: 'number', description: 'Number of objects', optional: true }, + objectTypeCount: { + type: 'number', + description: 'Number of object types', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Total number of schemas' }, + isLast: { type: 'boolean', description: 'Whether this is the last page' }, + }, +} diff --git a/apps/sim/tools/jsm/list_object_types.ts b/apps/sim/tools/jsm/list_object_types.ts new file mode 100644 index 00000000000..324963549c1 --- /dev/null +++ b/apps/sim/tools/jsm/list_object_types.ts @@ -0,0 +1,117 @@ +import type { JsmListObjectTypesParams, JsmListObjectTypesResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmListObjectTypesTool: ToolConfig< + JsmListObjectTypesParams, + JsmListObjectTypesResponse +> = { + id: 'jsm_list_object_types', + name: 'JSM List Asset Object Types', + description: 'List object types within an Assets (Insight/CMDB) object schema', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + schemaId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object schema ID to list object types for', + }, + excludeAbstract: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Exclude abstract object types from the result', + }, + }, + + request: { + url: '/api/tools/jsm/assets/object-types', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + schemaId: params.schemaId?.trim(), + excludeAbstract: params.excludeAbstract, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), objectTypes: [], total: 0 }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), objectTypes: [], total: 0 }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + objectTypes: { + type: 'array', + description: 'List of object types in the schema', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Object type ID' }, + name: { type: 'string', description: 'Object type name' }, + description: { type: 'string', description: 'Object type description', optional: true }, + objectSchemaId: { type: 'string', description: 'Parent schema ID' }, + objectCount: { type: 'number', description: 'Number of objects', optional: true }, + abstractObjectType: { + type: 'boolean', + description: 'Whether the type is abstract', + optional: true, + }, + inherited: { + type: 'boolean', + description: 'Whether the type inherits attributes', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Total number of object types' }, + }, +} diff --git a/apps/sim/tools/jsm/search_objects_aql.ts b/apps/sim/tools/jsm/search_objects_aql.ts new file mode 100644 index 00000000000..872cc90cc22 --- /dev/null +++ b/apps/sim/tools/jsm/search_objects_aql.ts @@ -0,0 +1,150 @@ +import type { JsmSearchObjectsAqlParams, JsmSearchObjectsAqlResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmSearchObjectsAqlTool: ToolConfig< + JsmSearchObjectsAqlParams, + JsmSearchObjectsAqlResponse +> = { + id: 'jsm_search_objects_aql', + name: 'JSM Search Assets (AQL)', + description: + 'Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = "Host" AND Status = "Running". Supports pagination.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + qlQuery: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'AQL query string (e.g., objectType = "Host" AND "Operating System" = "Ubuntu")', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based, defaults to 1)', + }, + resultsPerPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (e.g., 25, 50)', + }, + includeAttributes: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include resolved attribute values on each object (defaults to true)', + }, + objectTypeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optionally scope the search to a single object type ID', + }, + objectSchemaId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optionally scope the search to a single object schema ID', + }, + }, + + request: { + url: '/api/tools/jsm/assets/search', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + qlQuery: params.qlQuery, + page: params.page, + resultsPerPage: params.resultsPerPage, + includeAttributes: params.includeAttributes, + objectTypeId: params.objectTypeId, + objectSchemaId: params.objectSchemaId, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { + ts: new Date().toISOString(), + objects: [], + total: 0, + pageNumber: 0, + pageSize: 0, + }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { + ts: new Date().toISOString(), + objects: [], + total: 0, + pageNumber: 0, + pageSize: 0, + }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + objects: { + type: 'array', + description: 'Matching Assets objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Object ID' }, + label: { type: 'string', description: 'Object label', optional: true }, + objectKey: { type: 'string', description: 'Object key (e.g., HOST-123)', optional: true }, + objectType: { type: 'json', description: 'Object type metadata', optional: true }, + attributes: { type: 'json', description: 'Resolved attribute values', optional: true }, + }, + }, + }, + total: { type: 'number', description: 'Total number of matching objects (totalFilterCount)' }, + pageNumber: { type: 'number', description: 'Current page number' }, + pageSize: { type: 'number', description: 'Number of objects on this page' }, + }, +} diff --git a/apps/sim/tools/jsm/types.ts b/apps/sim/tools/jsm/types.ts index a0160854bba..e35c97c74c6 100644 --- a/apps/sim/tools/jsm/types.ts +++ b/apps/sim/tools/jsm/types.ts @@ -1080,3 +1080,229 @@ export type JsmResponse = | JsmCopyFormsResponse | JsmGetFormAnswersResponse | JsmReopenFormResponse + | JsmListObjectSchemasResponse + | JsmGetObjectSchemaResponse + | JsmListObjectTypesResponse + | JsmGetObjectTypeAttributesResponse + | JsmSearchObjectsAqlResponse + | JsmGetObjectResponse + | JsmCreateObjectResponse + | JsmUpdateObjectResponse + | JsmDeleteObjectResponse + +/** + * JSM Assets (Insight / CMDB) tool types. + * + * The Assets API is keyed by an Assets `workspaceId` (resolved server-side from + * the Jira `cloudId`). All tools share {@link JsmAssetsBaseParams}. + */ + +/** Base params shared by every JSM Assets tool */ +export interface JsmAssetsBaseParams { + accessToken: string + domain: string + /** Jira Cloud ID (resolved server-side from the domain when omitted) */ + cloudId?: string + /** Assets workspace ID (resolved server-side from the cloudId when omitted) */ + workspaceId?: string +} + +/** A single attribute value entry on an Assets object */ +export interface AssetObjectAttributeValue { + value: string | null + displayValue: string | null + searchValue?: string | null + referencedType?: boolean + referencedObject?: Record | null +} + +/** A resolved attribute on an Assets object (read shape) */ +export interface AssetObjectAttribute { + id: string + objectTypeAttributeId: string + objectAttributeValues: AssetObjectAttributeValue[] +} + +/** An Assets object as returned by get/create/update */ +export interface AssetObject { + id: string + label: string | null + objectKey: string | null + globalId: string | null + created: string | null + updated: string | null + hasAvatar: boolean + objectType: Record | null + attributes: AssetObjectAttribute[] + link: string | null +} + +/** Attribute payload for creating/updating an Assets object */ +export interface AssetObjectAttributeInput { + objectTypeAttributeId: string + objectAttributeValues: Array<{ value: unknown }> +} + +/** Raw attribute value as returned by the Assets API (before normalization) */ +export interface RawAssetObjectAttributeValue { + value?: string | null + displayValue?: string | null + searchValue?: string | null + referencedType?: boolean + referencedObject?: Record | null +} + +/** Raw attribute as returned by the Assets API (before normalization) */ +export interface RawAssetObjectAttribute { + id?: string + objectTypeAttributeId?: string + objectAttributeValues?: RawAssetObjectAttributeValue[] +} + +/** Raw Assets object as returned by get/create/update/AQL (before normalization) */ +export interface RawAssetObject { + id: string + label?: string | null + objectKey?: string | null + globalId?: string | null + created?: string | null + updated?: string | null + hasAvatar?: boolean + objectType?: Record | null + attributes?: RawAssetObjectAttribute[] + _links?: { self?: string } | null +} + +/** Output property descriptors reused across Assets object responses */ +export const ASSET_OBJECT_PROPERTIES = { + id: { type: 'string', description: 'Object ID' }, + label: { type: 'string', description: 'Human-readable object label', optional: true }, + objectKey: { type: 'string', description: 'Object key (e.g., HOST-123)', optional: true }, + globalId: { type: 'string', description: 'Global object ID', optional: true }, + objectType: { type: 'json', description: 'Object type metadata', optional: true }, + attributes: { type: 'json', description: 'Resolved attribute values for the object' }, + hasAvatar: { type: 'boolean', description: 'Whether the object has an avatar', optional: true }, + created: { type: 'string', description: 'Creation timestamp', optional: true }, + updated: { type: 'string', description: 'Last update timestamp', optional: true }, + link: { type: 'string', description: 'Self link to the object', optional: true }, +} as const + +export interface JsmListObjectSchemasParams extends JsmAssetsBaseParams { + startAt?: number + maxResults?: number + includeCounts?: boolean +} + +export interface JsmListObjectSchemasResponse extends ToolResponse { + output: { + ts: string + schemas: Array> + total: number + isLast: boolean + } +} + +export interface JsmGetObjectSchemaParams extends JsmAssetsBaseParams { + schemaId: string +} + +export interface JsmGetObjectSchemaResponse extends ToolResponse { + output: { + ts: string + schema: Record | null + } +} + +export interface JsmListObjectTypesParams extends JsmAssetsBaseParams { + schemaId: string + excludeAbstract?: boolean +} + +export interface JsmListObjectTypesResponse extends ToolResponse { + output: { + ts: string + objectTypes: Array> + total: number + } +} + +export interface JsmGetObjectTypeAttributesParams extends JsmAssetsBaseParams { + objectTypeId: string + onlyValueEditable?: boolean + query?: string +} + +export interface JsmGetObjectTypeAttributesResponse extends ToolResponse { + output: { + ts: string + attributes: Array> + total: number + } +} + +export interface JsmSearchObjectsAqlParams extends JsmAssetsBaseParams { + qlQuery: string + page?: number + resultsPerPage?: number + includeAttributes?: boolean + objectTypeId?: string + objectSchemaId?: string +} + +export interface JsmSearchObjectsAqlResponse extends ToolResponse { + output: { + ts: string + objects: Array> + total: number + pageNumber: number + pageSize: number + } +} + +export interface JsmGetObjectParams extends JsmAssetsBaseParams { + objectId: string +} + +export interface JsmGetObjectResponse extends ToolResponse { + output: { + ts: string + object: AssetObject | null + } +} + +export interface JsmCreateObjectParams extends JsmAssetsBaseParams { + objectTypeId: string + attributes: AssetObjectAttributeInput[] +} + +export interface JsmCreateObjectResponse extends ToolResponse { + output: { + ts: string + object: AssetObject | null + } +} + +export interface JsmUpdateObjectParams extends JsmAssetsBaseParams { + objectId: string + attributes: AssetObjectAttributeInput[] + objectTypeId?: string +} + +export interface JsmUpdateObjectResponse extends ToolResponse { + output: { + ts: string + object: AssetObject | null + } +} + +export interface JsmDeleteObjectParams extends JsmAssetsBaseParams { + objectId: string +} + +export interface JsmDeleteObjectResponse extends ToolResponse { + output: { + ts: string + objectId: string + deleted: boolean + } +} diff --git a/apps/sim/tools/jsm/update_object.ts b/apps/sim/tools/jsm/update_object.ts new file mode 100644 index 00000000000..4457cee9828 --- /dev/null +++ b/apps/sim/tools/jsm/update_object.ts @@ -0,0 +1,104 @@ +import type { JsmUpdateObjectParams, JsmUpdateObjectResponse } from '@/tools/jsm/types' +import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmUpdateObjectTool: ToolConfig = { + id: 'jsm_update_object', + name: 'JSM Update Asset Object', + description: + 'Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Assets workspace ID (resolved automatically when omitted)', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Assets object ID to update', + }, + attributes: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of attributes to set: [{ objectTypeAttributeId, objectAttributeValues: [{ value }] }]', + }, + objectTypeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional object type ID (only if changing the type)', + }, + }, + + request: { + url: '/api/tools/jsm/assets/object/update', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + workspaceId: params.workspaceId, + objectId: params.objectId?.trim(), + objectTypeId: params.objectTypeId?.trim(), + attributes: params.attributes, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), object: null }, + error: 'Empty response from API', + } + } + const data = JSON.parse(responseText) + if (data.success && data.output) return data + return { + success: data.success || false, + output: data.output || { ts: new Date().toISOString(), object: null }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + object: { + type: 'json', + description: 'The updated Assets object', + properties: ASSET_OBJECT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/jsm/utils.ts b/apps/sim/tools/jsm/utils.ts index aaf4dad250a..df9a46e4af3 100644 --- a/apps/sim/tools/jsm/utils.ts +++ b/apps/sim/tools/jsm/utils.ts @@ -2,6 +2,58 @@ * Shared utilities for Jira Service Management tools */ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { AssetObject, RawAssetObject } from '@/tools/jsm/types' + +/** + * Resolve the Jira `cloudId` and Assets `workspaceId` needed for an Assets API + * call, using the request params when present and falling back to discovery. + * @param domain - The Jira site domain + * @param accessToken - The OAuth access token + * @param cloudIdParam - Optional cloudId already supplied by the caller + * @param workspaceIdParam - Optional workspaceId already supplied by the caller + */ +export async function resolveAssetsContext( + domain: string, + accessToken: string, + cloudIdParam?: string, + workspaceIdParam?: string +): Promise<{ cloudId: string; workspaceId: string }> { + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + const workspaceId = workspaceIdParam || (await getAssetsWorkspaceId(cloudId, accessToken)) + return { cloudId, workspaceId } +} + +/** + * Normalize a raw Assets object (from get/create/update) into the trimmed + * {@link AssetObject} shape returned by the tools. + * @param data - The raw object payload from the Assets API + */ +export function mapAssetObject(data: RawAssetObject): AssetObject { + return { + id: data.id, + label: data.label ?? null, + objectKey: data.objectKey ?? null, + globalId: data.globalId ?? null, + created: data.created ?? null, + updated: data.updated ?? null, + hasAvatar: data.hasAvatar ?? false, + objectType: data.objectType ?? null, + attributes: (data.attributes ?? []).map((attr) => ({ + id: attr.id ?? '', + objectTypeAttributeId: attr.objectTypeAttributeId ?? '', + objectAttributeValues: (attr.objectAttributeValues ?? []).map((v) => ({ + value: v.value ?? null, + displayValue: v.displayValue ?? null, + searchValue: v.searchValue ?? null, + referencedType: v.referencedType ?? false, + referencedObject: v.referencedObject ?? null, + })), + })), + link: data._links?.self ?? null, + } +} + /** * Build the base URL for JSM Service Desk API * @param cloudId - The Jira Cloud ID @@ -33,3 +85,55 @@ export function getJsmHeaders(accessToken: string): Record { 'X-ExperimentalApi': 'opt-in', } } + +/** + * Build the base URL for the JSM Assets (Insight/CMDB) API. + * + * Uses the OAuth 2.0 (3LO) gateway form `/ex/jira/{cloudId}/...` — matching + * {@link getJsmApiBaseUrl} — keyed by both the Jira `cloudId` and the Assets + * `workspaceId` (resolved via {@link getAssetsWorkspaceId}). + * @param cloudId - The Jira Cloud ID + * @param workspaceId - The Assets workspace ID + * @returns The base URL for the Assets API (v1) + */ +export function getAssetsApiBaseUrl(cloudId: string, workspaceId: string): string { + return `https://api.atlassian.com/ex/jira/${cloudId}/jsm/assets/workspace/${workspaceId}/v1` +} + +/** + * Resolve the Assets `workspaceId` for a Jira site. + * + * Calls the Service Desk discovery endpoint and uses the first workspace. + * Atlassian provisions a single Assets workspace per site, so this is the + * canonical workspace; callers on a multi-workspace site can pass an explicit + * `workspaceId` to {@link resolveAssetsContext} to override it. Requires the + * `read:servicedesk-request` scope (already granted by the `jira` provider). + * @param cloudId - The Jira Cloud ID + * @param accessToken - The OAuth access token + * @returns The Assets workspace ID for the site + * @throws If discovery fails or no workspace is provisioned + */ +export async function getAssetsWorkspaceId(cloudId: string, accessToken: string): Promise { + const response = await fetch( + `https://api.atlassian.com/ex/jira/${cloudId}/rest/servicedeskapi/assets/workspace`, + { method: 'GET', headers: getJsmHeaders(accessToken) } + ) + + if (!response.ok) { + const errorText = await response.text() + throw new Error( + `Failed to resolve Assets workspace: ${response.status} - ${errorText || response.statusText}` + ) + } + + const data = await response.json() + const workspaceId: string | undefined = data?.values?.[0]?.workspaceId + + if (!workspaceId) { + throw new Error( + 'No Assets workspace found for this site. Assets (Insight) may not be enabled on the Jira instance.' + ) + } + + return workspaceId +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 45fef1980aa..1f069c3be05 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1691,9 +1691,11 @@ import { jsmAnswerApprovalTool, jsmAttachFormTool, jsmCopyFormsTool, + jsmCreateObjectTool, jsmCreateOrganizationTool, jsmCreateRequestTool, jsmDeleteFormTool, + jsmDeleteObjectTool, jsmExternaliseFormTool, jsmGetApprovalsTool, jsmGetCommentsTool, @@ -1703,6 +1705,9 @@ import { jsmGetFormTemplatesTool, jsmGetFormTool, jsmGetIssueFormsTool, + jsmGetObjectSchemaTool, + jsmGetObjectTool, + jsmGetObjectTypeAttributesTool, jsmGetOrganizationsTool, jsmGetParticipantsTool, jsmGetQueuesTool, @@ -1714,10 +1719,14 @@ import { jsmGetSlaTool, jsmGetTransitionsTool, jsmInternaliseFormTool, + jsmListObjectSchemasTool, + jsmListObjectTypesTool, jsmReopenFormTool, jsmSaveFormAnswersTool, + jsmSearchObjectsAqlTool, jsmSubmitFormTool, jsmTransitionRequestTool, + jsmUpdateObjectTool, } from '@/tools/jsm' import { kalshiAmendOrderTool, @@ -4228,6 +4237,15 @@ export const tools: Record = { jsm_externalise_form: jsmExternaliseFormTool, jsm_internalise_form: jsmInternaliseFormTool, jsm_copy_forms: jsmCopyFormsTool, + jsm_list_object_schemas: jsmListObjectSchemasTool, + jsm_get_object_schema: jsmGetObjectSchemaTool, + jsm_list_object_types: jsmListObjectTypesTool, + jsm_get_object_type_attributes: jsmGetObjectTypeAttributesTool, + jsm_search_objects_aql: jsmSearchObjectsAqlTool, + jsm_get_object: jsmGetObjectTool, + jsm_create_object: jsmCreateObjectTool, + jsm_update_object: jsmUpdateObjectTool, + jsm_delete_object: jsmDeleteObjectTool, kalshi_get_markets: kalshiGetMarketsTool, kalshi_get_markets_v2: kalshiGetMarketsV2Tool, kalshi_get_market: kalshiGetMarketTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index d82996804ca..f970921235e 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 828, - zodRoutes: 828, + totalRoutes: 837, + zodRoutes: 837, nonZodRoutes: 0, } as const From 4f4ff534111898723708755544bf9ddec05d639c Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 15 Jun 2026 16:45:55 -0700 Subject: [PATCH 08/24] fix(uploads): authorize internal file URLs before download (#5049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(uploads): authorize internal file URLs before download downloadFileFromUrl treated any URL containing /api/files/serve/ as trusted-internal and read the object straight from storage by key with no access check, while every other resolution path in the file calls verifyFileAccess. Reachable during workflow execution via file[] inputs (type: 'url'), letting an authenticated user read arbitrary storage objects across tenants by supplying a storage key. Thread the caller's userId into downloadFileFromUrl and run verifyFileAccess(key, userId, undefined, context, false) on the resolved key before downloadFile; fail closed when no userId is present. Update all callers (execution file inputs, tool file outputs, KB ingestion); webhook and chat inputs already thread userId via processExecutionFiles. * chore(uploads): log denied internal file downloads for rollout telemetry * fix(uploads): derive internal file context from key, not query param Cursor Bugbot flagged a context-spoofing bypass: downloadFileFromUrl resolved context via parseInternalFileUrl, which honors a caller-controlled ?context= query param. An attacker could label a private storage key with a world-readable context (profile-pictures/og-images/workspace-logos) so verifyFileAccess short-circuits to granted while downloadFile still reads the private object. Infer context from the key only (inferContextFromKey), mirroring how /api/files/serve resolves it; ignore the query param. Also move the userId guard ahead of key resolution so auth failures surface first. * docs(uploads): move context-derivation rationale into TSDoc * fix(uploads): match internal file marker in URL path only isInternalFileUrl matched the /api/files/serve/ substring anywhere in the string, so a crafted URL could carry it in a query string or fragment and skip DNS/SSRF validation. Match it in the path component only. The raw path is checked without URL normalization on purpose: the files parse route relies on traversal sequences surviving this check (an absolute https://host/api/files/serve/../.. URL must classify as internal so the '..' check rejects it, rather than being normalized to /etc/... and waved through as external). Host is intentionally not gated — cross-tenant reads are prevented at the storage sink by verifyFileAccess, and host-allowlisting would break self-hosted/multi-domain deployments. Adds unit tests. * consolidate access, billing principals --------- Co-authored-by: Vikhyath Mondreti --- .../sim/executor/utils/file-tool-processor.ts | 2 +- apps/sim/lib/execution/files.ts | 2 +- .../knowledge/documents/document-processor.ts | 45 ++++++++------ apps/sim/lib/knowledge/documents/service.ts | 8 ++- .../lib/uploads/utils/file-utils.server.ts | 61 ++++++++++++++++--- apps/sim/lib/uploads/utils/file-utils.test.ts | 37 ++++++++++- apps/sim/lib/uploads/utils/file-utils.ts | 27 +++++++- 7 files changed, 150 insertions(+), 32 deletions(-) diff --git a/apps/sim/executor/utils/file-tool-processor.ts b/apps/sim/executor/utils/file-tool-processor.ts index 1fa86ffc731..eabac4b35c8 100644 --- a/apps/sim/executor/utils/file-tool-processor.ts +++ b/apps/sim/executor/utils/file-tool-processor.ts @@ -138,7 +138,7 @@ export class FileToolProcessor { } if (!buffer && data.url) { - buffer = await downloadFileFromUrl(data.url) + buffer = await downloadFileFromUrl(data.url, { userId: context.userId }) } if (buffer) { diff --git a/apps/sim/lib/execution/files.ts b/apps/sim/lib/execution/files.ts index c0b69446fcf..e23582aba18 100644 --- a/apps/sim/lib/execution/files.ts +++ b/apps/sim/lib/execution/files.ts @@ -57,7 +57,7 @@ export async function processExecutionFile( if (file.type === 'url' && file.data) { const { downloadFileFromUrl } = await import('@/lib/uploads/utils/file-utils.server') - const buffer = await downloadFileFromUrl(file.data) + const buffer = await downloadFileFromUrl(file.data, { userId }) if (buffer.length > MAX_FILE_SIZE) { const fileSizeMB = (buffer.length / (1024 * 1024)).toFixed(2) diff --git a/apps/sim/lib/knowledge/documents/document-processor.ts b/apps/sim/lib/knowledge/documents/document-processor.ts index 33ee06eb11b..63f0bb7eae4 100644 --- a/apps/sim/lib/knowledge/documents/document-processor.ts +++ b/apps/sim/lib/knowledge/documents/document-processor.ts @@ -295,7 +295,7 @@ async function parseDocument( if (isPDF && (hasAzureMistralOCR || hasMistralOCR)) { if (hasAzureMistralOCR) { logger.info(`Using Azure Mistral OCR: ${filename}`) - return parseWithAzureMistralOCR(fileUrl, filename, mimeType) + return parseWithAzureMistralOCR(fileUrl, filename, mimeType, userId) } if (hasMistralOCR) { @@ -305,7 +305,7 @@ async function parseDocument( } logger.info(`Using file parser: ${filename}`) - return parseWithFileParser(fileUrl, filename, mimeType) + return parseWithFileParser(fileUrl, filename, mimeType, userId) } async function handleFileForOCR( @@ -321,7 +321,7 @@ async function handleFileForOCR( if (mimeType === 'application/pdf') { logger.info(`handleFileForOCR: Downloading external PDF to check page count`) try { - const buffer = await downloadFileWithTimeout(fileUrl) + const buffer = await downloadFileWithTimeout(fileUrl, userId) logger.info(`handleFileForOCR: Downloaded external PDF: ${buffer.length} bytes`) return { httpsUrl: fileUrl, buffer } } catch (error) { @@ -340,7 +340,7 @@ async function handleFileForOCR( logger.info(`Uploading "${filename}" to cloud storage for OCR`) - const buffer = await downloadFileWithTimeout(fileUrl) + const buffer = await downloadFileWithTimeout(fileUrl, userId) logger.info(`Downloaded ${filename}: ${buffer.length} bytes`) @@ -380,11 +380,11 @@ async function handleFileForOCR( } } -async function downloadFileWithTimeout(fileUrl: string): Promise { - return downloadFileFromUrl(fileUrl, TIMEOUTS.FILE_DOWNLOAD) +async function downloadFileWithTimeout(fileUrl: string, userId?: string): Promise { + return downloadFileFromUrl(fileUrl, { timeoutMs: TIMEOUTS.FILE_DOWNLOAD, userId }) } -async function downloadFileForBase64(fileUrl: string): Promise { +async function downloadFileForBase64(fileUrl: string, userId?: string): Promise { if (/^data:/i.test(fileUrl)) { const [, base64Data] = fileUrl.split(',') if (!base64Data) { @@ -393,7 +393,7 @@ async function downloadFileForBase64(fileUrl: string): Promise { return Buffer.from(base64Data, 'base64') } if (/^https?:\/\//i.test(fileUrl)) { - return downloadFileWithTimeout(fileUrl) + return downloadFileWithTimeout(fileUrl, userId) } throw new Error('Unsupported fileUrl scheme: only data: URIs and http(s):// URLs are allowed') } @@ -468,7 +468,12 @@ async function makeOCRRequest( } } -async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeType: string) { +async function parseWithAzureMistralOCR( + fileUrl: string, + filename: string, + mimeType: string, + userId?: string +) { validateOCRConfig( env.OCR_AZURE_API_KEY, env.OCR_AZURE_ENDPOINT, @@ -476,7 +481,7 @@ async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeT 'Azure Mistral OCR' ) - const fileBuffer = await downloadFileForBase64(fileUrl) + const fileBuffer = await downloadFileForBase64(fileUrl, userId) if (mimeType === 'application/pdf') { const pageCount = await getPdfPageCount(fileBuffer) @@ -485,7 +490,7 @@ async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeT `PDF has ${pageCount} pages, exceeds Azure OCR limit of ${MISTRAL_MAX_PAGES}. ` + `Falling back to file parser.` ) - return parseWithFileParser(fileUrl, filename, mimeType) + return parseWithFileParser(fileUrl, filename, mimeType, userId) } logger.info(`Azure Mistral OCR: PDF page count for ${filename}: ${pageCount}`) } @@ -529,7 +534,7 @@ async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeT }) logger.info(`Falling back to file parser: ${filename}`) - return parseWithFileParser(fileUrl, filename, mimeType) + return parseWithFileParser(fileUrl, filename, mimeType, userId) } } @@ -589,7 +594,7 @@ async function parseWithMistralOCR( }) logger.info(`Falling back to file parser: ${filename}`) - return parseWithFileParser(fileUrl, filename, mimeType) + return parseWithFileParser(fileUrl, filename, mimeType, userId) } } @@ -773,7 +778,12 @@ async function processMistralOCRInBatches( } } -async function parseWithFileParser(fileUrl: string, filename: string, mimeType: string) { +async function parseWithFileParser( + fileUrl: string, + filename: string, + mimeType: string, + userId?: string +) { try { let content: string let metadata: FileParseMetadata = {} @@ -781,7 +791,7 @@ async function parseWithFileParser(fileUrl: string, filename: string, mimeType: if (/^data:/i.test(fileUrl)) { content = await parseDataURI(fileUrl, filename, mimeType) } else if (/^https?:\/\//i.test(fileUrl)) { - const result = await parseHttpFile(fileUrl, filename, mimeType) + const result = await parseHttpFile(fileUrl, filename, mimeType, userId) content = result.content metadata = result.metadata || {} } else { @@ -820,9 +830,10 @@ async function parseDataURI(fileUrl: string, filename: string, mimeType: string) async function parseHttpFile( fileUrl: string, filename: string, - mimeType?: string + mimeType?: string, + userId?: string ): Promise<{ content: string; metadata?: FileParseMetadata }> { - const buffer = await downloadFileWithTimeout(fileUrl) + const buffer = await downloadFileWithTimeout(fileUrl, userId) const extension = resolveParserExtension(filename, mimeType) const result = await parseBuffer(buffer, extension) diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index 307db69bc7c..bdfe3e5d557 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -515,7 +515,6 @@ export async function processDocumentAsync( // KB config + workspace billing + doc tags in one JOIN (was 3 SELECTs). const contextRows = await db .select({ - userId: knowledgeBase.userId, workspaceId: knowledgeBase.workspaceId, chunkingConfig: knowledgeBase.chunkingConfig, embeddingModel: knowledgeBase.embeddingModel, @@ -644,7 +643,12 @@ export async function processDocumentAsync( kbConfig.maxSize, kbConfig.overlap, kbConfig.minSize, - ctx.userId, + // Authorize the source file (and run OCR/processing) as the billed + // actor — the uploader when known, else the workspace billed account — + // the same principal embeddings are billed to. Using the KB owner here + // would authorize an attacker-supplied internal fileUrl against the + // owner, letting a KB write-member ingest a file only the owner can read. + billingUserId, ctx.workspaceId, rawConfig?.strategy, rawConfig?.strategyOptions diff --git a/apps/sim/lib/uploads/utils/file-utils.server.ts b/apps/sim/lib/uploads/utils/file-utils.server.ts index f0fb7a606a6..b5460c8440a 100644 --- a/apps/sim/lib/uploads/utils/file-utils.server.ts +++ b/apps/sim/lib/uploads/utils/file-utils.server.ts @@ -1,6 +1,6 @@ 'use server' -import type { Logger } from '@sim/logger' +import { createLogger, type Logger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { @@ -25,6 +25,8 @@ import { import { verifyFileAccess } from '@/app/api/files/authorization' import type { UserFile } from '@/executor/types' +const logger = createLogger('FileUtilsServer') + /** * Result type for file input resolution */ @@ -138,19 +140,62 @@ export async function resolveFileInputToUrl( } /** - * Download a file from a URL (internal or external) - * For internal URLs, uses direct storage access (server-side only) - * For external URLs, validates DNS/SSRF and uses secure fetch with IP pinning + * Options for {@link downloadFileFromUrl}. + */ +export interface DownloadFileFromUrlOptions { + /** Download timeout for external URLs. Defaults to the max execution timeout. */ + timeoutMs?: number + /** Hard cap on the number of bytes read from the source. */ + maxBytes?: number + /** + * Principal the download is performed on behalf of. Required to authorize + * internal (`/api/files/serve/...`) URLs: the resolved storage key is checked + * with {@link verifyFileAccess} before any bytes are read. Without it, internal + * URLs are rejected (fail closed) so a `/api/files/serve/` substring can never + * be treated as implicitly trusted. + */ + userId?: string +} + +/** + * Download a file from a URL (internal or external). + * + * For internal URLs, uses direct storage access (server-side only) after + * authorizing the resolved storage key against `userId`. Context is derived + * from the key via {@link inferContextFromKey}, never from a caller-controlled + * `?context=` query param — trusting the param would let a private key be + * labeled with a world-readable context (e.g. profile-pictures) so + * {@link verifyFileAccess} short-circuits to granted while the private object is + * still read. This mirrors how `/api/files/serve` resolves context. + * + * For external URLs, validates DNS/SSRF and uses secure fetch with IP pinning. */ export async function downloadFileFromUrl( fileUrl: string, - timeoutMs = getMaxExecutionTimeout(), - maxBytes?: number + options: DownloadFileFromUrlOptions = {} ): Promise { - const { parseInternalFileUrl } = await import('./file-utils') + const { timeoutMs = getMaxExecutionTimeout(), maxBytes, userId } = options if (isInternalFileUrl(fileUrl)) { - const { key, context } = parseInternalFileUrl(fileUrl) + if (!userId) { + logger.warn('Internal file download denied: no userId provided', { fileUrl }) + throw new Error('Access denied: internal file URL requires an authenticated user') + } + + const key = extractStorageKey(fileUrl) + if (!key) { + logger.warn('Internal file download denied: could not resolve storage key', { fileUrl }) + throw new Error('Access denied: could not resolve internal file key') + } + + const context = inferContextFromKey(key) + + const hasAccess = await verifyFileAccess(key, userId, undefined, context, false) + if (!hasAccess) { + logger.warn('Internal file download denied: access check failed', { key, context, userId }) + throw new Error('Access denied: file not found or insufficient permissions') + } + const { downloadFile } = await import('@/lib/uploads/core/storage-service') return downloadFile({ key, context, maxBytes }) } diff --git a/apps/sim/lib/uploads/utils/file-utils.test.ts b/apps/sim/lib/uploads/utils/file-utils.test.ts index f5e3c45ebde..63793e0e144 100644 --- a/apps/sim/lib/uploads/utils/file-utils.test.ts +++ b/apps/sim/lib/uploads/utils/file-utils.test.ts @@ -2,7 +2,42 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { isAbortError, isNetworkError } from '@/lib/uploads/utils/file-utils' +import { isAbortError, isInternalFileUrl, isNetworkError } from '@/lib/uploads/utils/file-utils' + +describe('isInternalFileUrl', () => { + it('classifies relative serve paths as internal', () => { + expect(isInternalFileUrl('/api/files/serve/kb/123-file.pdf')).toBe(true) + expect(isInternalFileUrl('/api/files/serve/workspace/ws-1/file.txt?context=workspace')).toBe( + true + ) + }) + + it('classifies absolute serve URLs as internal regardless of host', () => { + expect(isInternalFileUrl('https://www.sim.ai/api/files/serve/kb/x.pdf')).toBe(true) + expect(isInternalFileUrl('http://localhost:3000/api/files/serve/blob/kb/x')).toBe(true) + // Host is not used to gate (self-hosted/multi-domain); the storage sink authorizes. + expect(isInternalFileUrl('https://other-host/api/files/serve/workspace/v/x')).toBe(true) + }) + + it('does not match the marker outside the path (query/fragment)', () => { + expect(isInternalFileUrl('https://evil.com/x?next=/api/files/serve/secret')).toBe(false) + expect(isInternalFileUrl('https://evil.com/page#/api/files/serve/secret')).toBe(false) + expect(isInternalFileUrl('https://evil.com/redirect?u=/api/files/serve/kb/x')).toBe(false) + }) + + it('preserves traversal sequences so they survive downstream rejection', () => { + // Must stay internal (not normalized away) so the parse route applies its `..` check. + expect(isInternalFileUrl('https://attacker.com/api/files/serve/../../../etc/passwd')).toBe(true) + expect(isInternalFileUrl('/api/files/serve/../../app.js')).toBe(true) + }) + + it('returns false for non-internal and non-string inputs', () => { + expect(isInternalFileUrl('https://example.com/file.pdf')).toBe(false) + expect(isInternalFileUrl('data:text/plain;base64,abc')).toBe(false) + // @ts-expect-error verifying runtime guard + expect(isInternalFileUrl(undefined)).toBe(false) + }) +}) describe('isAbortError', () => { it('returns true for AbortError-named errors', () => { diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index d11fdf1b70f..e99d83c0564 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -534,10 +534,33 @@ export function extractStorageKey(filePath: string): string { } /** - * Check if a URL is an internal file serve URL + * Whether a URL targets the internal file-serve endpoint (`/api/files/serve/`). + * + * The marker is matched only in the URL's path component, so it cannot be + * smuggled through a query string or fragment (e.g. + * `https://evil.com/x?next=/api/files/serve/...`) to skip DNS/SSRF validation. + * + * The raw path is inspected without URL normalization on purpose: callers such + * as the files parse route rely on traversal sequences (`..`) surviving this + * check so they are rejected downstream rather than collapsed away. A path-only + * marker still classifies any host as internal (e.g. + * `https://other-host/api/files/serve/`); cross-tenant reads are prevented + * at the storage sink by {@link verifyFileAccess}, not by host matching, which + * would break self-hosted and multi-domain deployments. */ export function isInternalFileUrl(fileUrl: string): boolean { - return fileUrl.includes('/api/files/serve/') + if (typeof fileUrl !== 'string') { + return false + } + + let path = fileUrl + const scheme = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]*/i.exec(path) + if (scheme) { + path = path.slice(scheme[0].length) + } + path = path.split(/[?#]/, 1)[0] + + return path.startsWith('/api/files/serve/') } /** From a49e7554183193d435ff455eb077f9fa782c9fc8 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 15 Jun 2026 16:47:44 -0700 Subject: [PATCH 09/24] feat(auth): OAuth-only signup with Microsoft provider (#5073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(auth): OAuth-only signup with Microsoft provider - Remove email/password form from /signup — Google, Microsoft, GitHub OAuth only - Add Microsoft as a social provider (MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET / DISABLE_MICROSOFT_AUTH) - Wire microsoftAvailable through provider checker, API contract, providers route, and all auth UI - Hide "Continue with email" in auth modal signup view; login view unchanged - Fix MicrosoftIcon SVG to use official brand colors and proportions * fix(auth): remove unused useSession, guard invalid-callback warn with ref * feat(auth): gate email signup via DISABLE_EMAIL_SIGNUP flag * feat(auth): restore signup email form gated by NEXT_PUBLIC_DISABLE_EMAIL_SIGNUP * refactor(auth): single DISABLE_EMAIL_SIGNUP env var controls both ui and backend * fix(config): restore isHosted hostname check --- .../components/oauth-provider-checker.tsx | 12 +++- .../components/social-login-buttons.tsx | 43 ++++++++++++- apps/sim/app/(auth)/login/login-form.tsx | 5 +- apps/sim/app/(auth)/login/page.tsx | 4 +- apps/sim/app/(auth)/signup/page.tsx | 7 ++- apps/sim/app/(auth)/signup/signup-form.tsx | 63 +++++++++---------- .../components/auth-modal/auth-modal.tsx | 31 +++++++-- apps/sim/app/api/auth/providers/route.ts | 3 +- apps/sim/components/icons.tsx | 11 ++-- apps/sim/lib/api/contracts/auth.ts | 1 + apps/sim/lib/auth/auth.ts | 16 +++++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/core/config/feature-flags.ts | 13 ++++ 13 files changed, 155 insertions(+), 56 deletions(-) diff --git a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx index 73a95f98b02..218377f07de 100644 --- a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx +++ b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx @@ -1,5 +1,10 @@ import { env } from '@/lib/core/config/env' -import { isGithubAuthDisabled, isGoogleAuthDisabled, isProd } from '@/lib/core/config/feature-flags' +import { + isGithubAuthDisabled, + isGoogleAuthDisabled, + isMicrosoftAuthDisabled, + isProd, +} from '@/lib/core/config/feature-flags' export async function getOAuthProviderStatus() { const githubAvailable = @@ -8,5 +13,8 @@ export async function getOAuthProviderStatus() { const googleAvailable = !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) && !isGoogleAuthDisabled - return { githubAvailable, googleAvailable, isProduction: isProd } + const microsoftAvailable = + !!(env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) && !isMicrosoftAuthDisabled + + return { githubAvailable, googleAvailable, microsoftAvailable, isProduction: isProd } } diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 674ebe2eeb0..ed46b9413aa 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -2,12 +2,13 @@ import { type ReactNode, useState } from 'react' import { Button } from '@/components/emcn' -import { GithubIcon, GoogleIcon } from '@/components/icons' +import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons' import { client } from '@/lib/auth/auth-client' interface SocialLoginButtonsProps { githubAvailable: boolean googleAvailable: boolean + microsoftAvailable: boolean callbackURL?: string isProduction: boolean children?: ReactNode @@ -16,12 +17,14 @@ interface SocialLoginButtonsProps { export function SocialLoginButtons({ githubAvailable, googleAvailable, + microsoftAvailable, callbackURL = '/workspace', isProduction, children, }: SocialLoginButtonsProps) { const [isGithubLoading, setIsGithubLoading] = useState(false) const [isGoogleLoading, setIsGoogleLoading] = useState(false) + const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false) async function signInWithGithub() { if (!githubAvailable) return @@ -69,6 +72,29 @@ export function SocialLoginButtons({ } } + async function signInWithMicrosoft() { + if (!microsoftAvailable) return + + setIsMicrosoftLoading(true) + try { + await client.signIn.social({ provider: 'microsoft', callbackURL }) + } catch (err: any) { + let errorMessage = 'Failed to sign in with Microsoft' + + if (err.message?.includes('account exists')) { + errorMessage = 'An account with this email already exists. Please sign in instead.' + } else if (err.message?.includes('cancelled')) { + errorMessage = 'Microsoft sign in was cancelled. Please try again.' + } else if (err.message?.includes('network')) { + errorMessage = 'Network error. Please check your connection and try again.' + } else if (err.message?.includes('rate limit')) { + errorMessage = 'Too many attempts. Please try again later.' + } + } finally { + setIsMicrosoftLoading(false) + } + } + const githubButton = ( + ) + + const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable if (!hasAnyOAuthProvider && !children) { return null @@ -102,6 +140,7 @@ export function SocialLoginButtons({ return (
{googleAvailable && googleButton} + {microsoftAvailable && microsoftButton} {githubAvailable && githubButton} {children}
diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 67ac09b9461..de314167c7e 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -78,10 +78,12 @@ const validatePassword = (passwordValue: string): string[] => { export default function LoginPage({ githubAvailable, googleAvailable, + microsoftAvailable, isProduction, }: { githubAvailable: boolean googleAvailable: boolean + microsoftAvailable: boolean isProduction: boolean }) { const router = useRouter() @@ -335,7 +337,7 @@ export default function LoginPage({ const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) - const hasSocial = githubAvailable || googleAvailable + const hasSocial = githubAvailable || googleAvailable || microsoftAvailable const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial const showTopSSO = hasOnlySSO const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO) @@ -483,6 +485,7 @@ export default function LoginPage({
diff --git a/apps/sim/app/(auth)/signup/page.tsx b/apps/sim/app/(auth)/signup/page.tsx index 1f01e004643..b43f6ebad56 100644 --- a/apps/sim/app/(auth)/signup/page.tsx +++ b/apps/sim/app/(auth)/signup/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { isRegistrationDisabled } from '@/lib/core/config/feature-flags' +import { isEmailSignupDisabled, isRegistrationDisabled } from '@/lib/core/config/feature-flags' import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' import SignupForm from '@/app/(auth)/signup/signup-form' @@ -14,13 +14,16 @@ export default async function SignupPage() { return
Registration is disabled, please contact your admin.
} - const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus() + const { githubAvailable, googleAvailable, microsoftAvailable, isProduction } = + await getOAuthProviderStatus() return ( ) } diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 90490160dff..ae73e36cb5a 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -75,10 +75,18 @@ const validateEmailField = (emailValue: string): string[] => { interface SignupFormProps { githubAvailable: boolean googleAvailable: boolean + microsoftAvailable: boolean isProduction: boolean + emailSignupEnabled: boolean } -function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) { +function SignupFormContent({ + githubAvailable, + googleAvailable, + microsoftAvailable, + isProduction, + emailSignupEnabled, +}: SignupFormProps) { const router = useRouter() const searchParams = useSearchParams() const { refetch: refetchSession } = useSession() @@ -346,6 +354,14 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S } } + const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) + const emailEnabled = + !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && emailSignupEnabled + const hasSocial = githubAvailable || googleAvailable || microsoftAvailable + const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial + const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO) + const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection + return ( <>
@@ -357,21 +373,13 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S

- {/* SSO Login Button (primary top-only when it is the only method) */} - {(() => { - const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) - const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) - const hasSocial = githubAvailable || googleAvailable - const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial - return hasOnlySSO - })() && ( + {hasOnlySSO && (
)} - {/* Email/Password Form - show unless explicitly disabled */} - {!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && ( + {emailEnabled && (
@@ -540,16 +548,7 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S )} - {/* Divider - show when we have multiple auth methods */} - {(() => { - const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) - const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) - const hasSocial = githubAvailable || googleAvailable - const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial - const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO) - const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection - return showDivider - })() && ( + {showDivider && (
@@ -562,26 +561,16 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S
)} - {(() => { - const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) - const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) - const hasSocial = githubAvailable || googleAvailable - const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial - const showBottomSection = hasSocial || (ssoEnabled && !hasOnlySSO) - return showBottomSection - })() && ( -
+ {showBottomSection && ( +
- {isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && ( + {ssoEnabled && !hasOnlySSO && ( )} @@ -625,14 +614,18 @@ function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: S export default function SignupPage({ githubAvailable, googleAvailable, + microsoftAvailable, isProduction, + emailSignupEnabled, }: SignupFormProps) { return ( Loading…
}> ) diff --git a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx index f64ee69b34c..d0a1a985ac0 100644 --- a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx +++ b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx @@ -14,7 +14,7 @@ import { ModalTitle, ModalTrigger, } from '@/components/emcn' -import { GithubIcon, GoogleIcon } from '@/components/icons' +import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons' import { requestJson } from '@/lib/api/client/request' import { type AuthProviderStatusResponse, getAuthProvidersContract } from '@/lib/api/contracts/auth' import { client } from '@/lib/auth/auth-client' @@ -40,6 +40,7 @@ let fetchPromise: Promise | null = null const FALLBACK_STATUS: ProviderStatus = { githubAvailable: false, googleAvailable: false, + microsoftAvailable: false, registrationDisabled: false, } @@ -49,9 +50,10 @@ const SOCIAL_BTN = function fetchProviderStatus(): Promise { if (fetchPromise) return fetchPromise fetchPromise = requestJson(getAuthProvidersContract, {}) - .then(({ githubAvailable, googleAvailable, registrationDisabled }) => ({ + .then(({ githubAvailable, googleAvailable, microsoftAvailable, registrationDisabled }) => ({ githubAvailable, googleAvailable, + microsoftAvailable, registrationDisabled, })) .catch(() => { @@ -66,14 +68,17 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal const [open, setOpen] = useState(false) const [view, setView] = useState(defaultView) const [providerStatus, setProviderStatus] = useState(null) - const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null) + const [socialLoading, setSocialLoading] = useState<'github' | 'google' | 'microsoft' | null>(null) const brand = useMemo(() => getBrandConfig(), []) useEffect(() => { fetchProviderStatus().then(setProviderStatus) }, []) - const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable + const hasSocial = + providerStatus?.githubAvailable || + providerStatus?.googleAvailable || + providerStatus?.microsoftAvailable const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) const hasModalContent = hasSocial || ssoEnabled @@ -104,7 +109,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal } } - async function handleSocialLogin(provider: 'github' | 'google') { + async function handleSocialLogin(provider: 'github' | 'google' | 'microsoft') { setSocialLoading(provider) try { await client.signIn.social({ provider, callbackURL: '/workspace' }) @@ -184,6 +189,19 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal )} + {providerStatus.microsoftAvailable && ( + + )} {providerStatus.githubAvailable && (
- - handleRemoveMember(member.id)} - disabled={removeMember.isPending} - className='flex-shrink-0' - > - Remove - -
- ) - })} + + + + + handleRemoveMember(member.id)} + > + Remove + + + + } + /> + ))}
)} -
+
@@ -1468,51 +1647,68 @@ export function AccessControl() {
-
- setSearchTerm(e.target.value)} - /> +
+
+

Access Control

+

+ Manage permission groups across every workspace in your organization. +

+
- {filteredGroups.length === 0 && searchTerm.trim() ? ( -
- No results found matching "{searchTerm}" -
- ) : permissionGroups.length === 0 ? ( -
- Click "Create Group" above to get started -
- ) : ( -
- {filteredGroups.map((group) => ( -
- - - ))} -
- )} + + + ))} +
+ )} +
@@ -1547,13 +1743,27 @@ export function AccessControl() { setNewGroupIsDefault(checked === true)} + onCheckedChange={(checked) => { + const isDefault = checked === true + setNewGroupIsDefault(isDefault) + if (isDefault) setNewGroupWorkspaceIds([]) + }} /> + + + {createError} [...permissionGroupKeys.all, 'userConfig', workspaceId ?? ''] as const, + orgWorkspaces: (organizationId?: string) => + [...permissionGroupKeys.all, 'orgWorkspaces', organizationId ?? ''] as const, } export function usePermissionGroups(organizationId?: string, enabled = true) { @@ -65,6 +74,22 @@ export function usePermissionGroupMembers(organizationId?: string, permissionGro }) } +export function useOrganizationWorkspaces(organizationId?: string, enabled = true) { + return useQuery({ + queryKey: permissionGroupKeys.orgWorkspaces(organizationId), + queryFn: async ({ signal }) => { + if (!organizationId) return [] + const data = await requestJson(listOrganizationWorkspacesContract, { + params: { id: organizationId }, + signal, + }) + return data.workspaces + }, + enabled: Boolean(organizationId) && enabled, + staleTime: 60 * 1000, + }) +} + export function useUserPermissionConfig(workspaceId?: string) { return useQuery({ queryKey: permissionGroupKeys.userConfig(workspaceId), @@ -86,6 +111,8 @@ export interface CreatePermissionGroupData { description?: string config?: Partial isDefault?: boolean + appliesToAllWorkspaces?: boolean + workspaceIds?: string[] } export function useCreatePermissionGroup() { @@ -113,6 +140,8 @@ export interface UpdatePermissionGroupData { description?: string | null config?: Partial isDefault?: boolean + appliesToAllWorkspaces?: boolean + workspaceIds?: string[] } export function useUpdatePermissionGroup() { diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index 7476110a896..467a46d851b 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -11,6 +11,7 @@ const { mockGetProviderFromModel, mockGetBlock, mockExplicitGroup, + mockAllWorkspacesGroup, mockDefaultGroup, } = vi.hoisted(() => ({ DEFAULT_PERMISSION_GROUP_CONFIG: { @@ -42,30 +43,53 @@ const { mockGetWorkspaceWithOwner: vi.fn<() => Promise<{ organizationId: string | null } | null>>(), mockGetProviderFromModel: vi.fn<(model: string) => string>(), mockGetBlock: vi.fn<(type: string) => { hideFromToolbar?: boolean } | undefined>(), - // The explicit-group query joins permission_group_member -> permission_group; - // the org-default query selects permission_group directly. The db mock returns - // the explicit rows when `innerJoin` was called and the default rows otherwise. - mockExplicitGroup: { value: [] as Array<{ config: Record }> }, + // resolveWorkspaceGroup joins member -> group -> LEFT JOIN group_workspace and + // awaits the `where` directly (no limit). resolveOrganizationWideGroup joins + // member -> group (inner only) with limit. resolveDefaultGroup selects group + // directly with limit. The db mock branches on which joins were used: + // leftJoin -> mockExplicitGroup (the user's group rows) + // innerJoin (no left) -> mockAllWorkspacesGroup (org-wide member group) + // no join -> mockDefaultGroup (the org default) + mockExplicitGroup: { + value: [] as Array<{ + id?: string + name?: string + config: Record + appliesToAllWorkspaces?: boolean + targetsWorkspace?: string | null + }>, + }, + mockAllWorkspacesGroup: { value: [] as Array<{ config: Record }> }, mockDefaultGroup: { value: [] as Array<{ config: Record }> }, })) vi.mock('@sim/db', () => ({ db: { select: vi.fn().mockImplementation(() => { - const chain: Record = {} let usedInnerJoin = false + let usedLeftJoin = false + const resolveRows = () => { + if (usedLeftJoin) return mockExplicitGroup.value + if (usedInnerJoin) return mockAllWorkspacesGroup.value + return mockDefaultGroup.value + } + const chain: Record = {} chain.from = vi.fn().mockReturnValue(chain) chain.innerJoin = vi.fn().mockImplementation(() => { usedInnerJoin = true return chain }) + chain.leftJoin = vi.fn().mockImplementation(() => { + usedLeftJoin = true + return chain + }) chain.where = vi.fn().mockReturnValue(chain) chain.orderBy = vi.fn().mockReturnValue(chain) - chain.limit = vi - .fn() - .mockImplementation(() => - Promise.resolve(usedInnerJoin ? mockExplicitGroup.value : mockDefaultGroup.value) - ) + chain.limit = vi.fn().mockImplementation(() => Promise.resolve(resolveRows())) + // resolveWorkspaceGroup awaits the builder directly after `where` (no limit), + // so the chain must be thenable. + chain.then = (onFulfilled: (rows: unknown) => unknown) => + Promise.resolve(resolveRows()).then(onFulfilled) return chain }), }, @@ -74,6 +98,7 @@ vi.mock('@sim/db', () => ({ vi.mock('@sim/db/schema', () => ({ permissionGroup: {}, permissionGroupMember: {}, + permissionGroupWorkspace: {}, })) vi.mock('drizzle-orm', () => ({ @@ -164,6 +189,7 @@ describe('getUserPermissionConfig (org-scoped resolution)', () => { beforeEach(() => { vi.clearAllMocks() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) }) @@ -227,6 +253,7 @@ describe('getUserPermissionConfig (org-scoped resolution)', () => { it('returns null when there is no explicit group and no default group', async () => { setEnterpriseOrgWorkspace() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] const config = await getUserPermissionConfig('user-123', 'workspace-1') @@ -246,10 +273,90 @@ describe('getUserPermissionConfig (org-scoped resolution)', () => { }) }) +describe('getUserPermissionConfig (workspace-scope precedence)', () => { + beforeEach(() => { + vi.clearAllMocks() + mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] + mockDefaultGroup.value = [] + mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) + setEnterpriseOrgWorkspace() + }) + + it('prefers a specific group covering the workspace over an all-workspaces group', async () => { + mockExplicitGroup.value = [ + { + id: 'all', + name: 'All', + config: { disableMcpTools: true }, + appliesToAllWorkspaces: true, + targetsWorkspace: null, + }, + { + id: 'specific', + name: 'Specific', + config: { disableSkills: true }, + appliesToAllWorkspaces: false, + targetsWorkspace: 'workspace-1', + }, + ] + + const config = await getUserPermissionConfig('user-123', 'workspace-1') + + expect(config?.disableSkills).toBe(true) + expect(config?.disableMcpTools).toBe(false) + }) + + it('uses the all-workspaces group when no specific group covers the workspace', async () => { + mockExplicitGroup.value = [ + { + id: 'all', + name: 'All', + config: { disableMcpTools: true }, + appliesToAllWorkspaces: true, + targetsWorkspace: null, + }, + { + // A specific group the user is in, but it does not target this workspace + // (left join produced no row, so targetsWorkspace is null). + id: 'specific-other', + name: 'Specific Other', + config: { disableSkills: true }, + appliesToAllWorkspaces: false, + targetsWorkspace: null, + }, + ] + + const config = await getUserPermissionConfig('user-123', 'workspace-1') + + expect(config?.disableMcpTools).toBe(true) + expect(config?.disableSkills).toBe(false) + }) + + it('falls back to the org default when the user has only a non-covering specific group', async () => { + mockExplicitGroup.value = [ + { + id: 'specific-other', + name: 'Specific Other', + config: { disableSkills: true }, + appliesToAllWorkspaces: false, + targetsWorkspace: null, + }, + ] + mockDefaultGroup.value = [{ config: { disableCustomTools: true } }] + + const config = await getUserPermissionConfig('user-123', 'workspace-1') + + expect(config?.disableCustomTools).toBe(true) + expect(config?.disableSkills).toBe(false) + }) +}) + describe('validateBlockType', () => { beforeEach(() => { vi.clearAllMocks() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] }) @@ -327,6 +434,7 @@ describe('validateModelProvider', () => { beforeEach(() => { vi.clearAllMocks() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) setEnterpriseOrgWorkspace() @@ -402,6 +510,7 @@ describe('validateMcpToolsAllowed', () => { beforeEach(() => { vi.clearAllMocks() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) setEnterpriseOrgWorkspace() @@ -426,6 +535,7 @@ describe('assertPermissionsAllowed', () => { beforeEach(() => { vi.clearAllMocks() mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] mockGetAllowedIntegrationsFromEnv.mockReturnValue(null) setEnterpriseOrgWorkspace() @@ -508,6 +618,7 @@ describe('assertPermissionsAllowed', () => { it('passes when the workspace has no blocking config', async () => { mockExplicitGroup.value = [] + mockAllWorkspacesGroup.value = [] mockDefaultGroup.value = [] await assertPermissionsAllowed({ diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index 5d0cbbac606..1fbc647435a 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' -import { permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { permissionGroup, permissionGroupMember, permissionGroupWorkspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' +import { and, asc, eq } from 'drizzle-orm' import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import { getAllowedIntegrationsFromEnv, @@ -112,56 +112,149 @@ function mergeEnvAllowlist(config: PermissionGroupConfig | null): PermissionGrou } /** - * Resolve the raw (pre-env-merge) permission-group config that governs `userId` - * within `organizationId`. Resolution is deterministic with no workspace-level - * fallbacks: - * 1. the user's explicitly assigned group in the organization, else - * 2. the organization's default group (`isDefault`), which also governs - * external members operating in the org's workspaces, else - * 3. `null` (unrestricted). + * The permission group that governs a user in a given context, with its parsed + * config. Shared by the executor path and the `/api/permission-groups/user` + * route so resolution never drifts between the two. + */ +export interface ResolvedPermissionGroup { + permissionGroupId: string + groupName: string + config: PermissionGroupConfig +} + +/** The organization's single default group (`isDefault`), or `null`. */ +async function resolveDefaultGroup( + organizationId: string +): Promise { + const [defaultGroup] = await db + .select({ + id: permissionGroup.id, + name: permissionGroup.name, + config: permissionGroup.config, + }) + .from(permissionGroup) + .where( + and(eq(permissionGroup.organizationId, organizationId), eq(permissionGroup.isDefault, true)) + ) + .limit(1) + + if (!defaultGroup) { + return null + } + + return { + permissionGroupId: defaultGroup.id, + groupName: defaultGroup.name, + config: parsePermissionGroupConfig(defaultGroup.config), + } +} + +/** + * Resolve the group governing `userId` in `workspaceId` (which belongs to + * `organizationId`). Deterministic precedence, one effective group per + * workspace: + * 1. a specific-scope group the user is in that targets this workspace, else + * 2. the user's all-workspaces group, else + * 3. the organization's default group (also governs external members), else + * 4. `null` (unrestricted). + * + * Specific-scope groups a user belongs to should not overlap on a workspace, + * and a user should belong to at most one all-workspaces group (enforced at + * assignment time, though not by a DB constraint). If an overlap nonetheless + * exists, the oldest group wins — rows are ordered by `created_at` (then `id`) + * so resolution is deterministic. * - * Callers are responsible for the enterprise-entitlement gate before invoking - * this and for merging the env allowlist afterwards. + * Callers gate on enterprise entitlement before invoking this and merge the env + * allowlist afterwards. */ -async function resolveOrganizationGroupConfig( +export async function resolveWorkspaceGroup( userId: string, - organizationId: string -): Promise { - const [explicit] = await db - .select({ config: permissionGroup.config }) + organizationId: string, + workspaceId: string +): Promise { + const rows = await db + .select({ + id: permissionGroup.id, + name: permissionGroup.name, + config: permissionGroup.config, + appliesToAllWorkspaces: permissionGroup.appliesToAllWorkspaces, + // Non-null only when this group has a specific row targeting the workspace. + targetsWorkspace: permissionGroupWorkspace.workspaceId, + }) .from(permissionGroupMember) .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .leftJoin( + permissionGroupWorkspace, + and( + eq(permissionGroupWorkspace.permissionGroupId, permissionGroup.id), + eq(permissionGroupWorkspace.workspaceId, workspaceId) + ) + ) .where( and( eq(permissionGroupMember.userId, userId), eq(permissionGroupMember.organizationId, organizationId) ) ) - .limit(1) + .orderBy(asc(permissionGroup.createdAt), asc(permissionGroup.id)) + + const specific = rows.find((row) => !row.appliesToAllWorkspaces && row.targetsWorkspace !== null) + const winner = specific ?? rows.find((row) => row.appliesToAllWorkspaces) - if (explicit) { - return parsePermissionGroupConfig(explicit.config) + if (winner) { + return { + permissionGroupId: winner.id, + groupName: winner.name, + config: parsePermissionGroupConfig(winner.config), + } } - const [defaultGroup] = await db - .select({ config: permissionGroup.config }) - .from(permissionGroup) + return resolveDefaultGroup(organizationId) +} + +/** + * Organization-level resolution (no specific workspace in context, e.g. + * organization-wide invitations): the user's all-workspaces group, else the + * organization default. Specific-scope groups require a workspace and therefore + * do not gate organization-level actions. + */ +export async function resolveOrganizationWideGroup( + userId: string, + organizationId: string +): Promise { + const [allWorkspacesGroup] = await db + .select({ + id: permissionGroup.id, + name: permissionGroup.name, + config: permissionGroup.config, + }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) .where( - and(eq(permissionGroup.organizationId, organizationId), eq(permissionGroup.isDefault, true)) + and( + eq(permissionGroupMember.userId, userId), + eq(permissionGroupMember.organizationId, organizationId), + eq(permissionGroup.appliesToAllWorkspaces, true) + ) ) + .orderBy(asc(permissionGroup.createdAt), asc(permissionGroup.id)) .limit(1) - if (defaultGroup) { - return parsePermissionGroupConfig(defaultGroup.config) + if (allWorkspacesGroup) { + return { + permissionGroupId: allWorkspacesGroup.id, + groupName: allWorkspacesGroup.name, + config: parsePermissionGroupConfig(allWorkspacesGroup.config), + } } - return null + return resolveDefaultGroup(organizationId) } /** * Resolve the effective permission-group config for a user in the context of a * specific workspace. The workspace is mapped to its organization and the - * org-scoped config is resolved (explicit group -> org default -> none). + * governing group is resolved with specific-over-all precedence. * * Returns `null` (after env merge) when the workspace has no organization, the * organization isn't on an enterprise plan, or no group governs the user. @@ -182,13 +275,19 @@ export async function getUserPermissionConfig( return mergeEnvAllowlist(null) } - return getUserPermissionConfigForOrganization(userId, ws.organizationId) + const isEnterprise = await isOrganizationOnEnterprisePlan(ws.organizationId) + if (!isEnterprise) { + return mergeEnvAllowlist(null) + } + + const resolved = await resolveWorkspaceGroup(userId, ws.organizationId, workspaceId) + return mergeEnvAllowlist(resolved?.config ?? null) } /** - * Org-addressed variant of {@link getUserPermissionConfig}. Use when the - * organization id is already known (e.g. organization-level invitations) so no - * workspace -> organization lookup is needed. + * Org-addressed variant of {@link getUserPermissionConfig}. Use when only the + * organization is known (e.g. organization-level invitations); resolves the + * user's all-workspaces group or the org default. */ export async function getUserPermissionConfigForOrganization( userId: string, @@ -203,7 +302,8 @@ export async function getUserPermissionConfigForOrganization( return mergeEnvAllowlist(null) } - return mergeEnvAllowlist(await resolveOrganizationGroupConfig(userId, organizationId)) + const resolved = await resolveOrganizationWideGroup(userId, organizationId) + return mergeEnvAllowlist(resolved?.config ?? null) } /** diff --git a/apps/sim/lib/api/contracts/permission-groups.test.ts b/apps/sim/lib/api/contracts/permission-groups.test.ts new file mode 100644 index 00000000000..b8f506631c4 --- /dev/null +++ b/apps/sim/lib/api/contracts/permission-groups.test.ts @@ -0,0 +1,125 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + createPermissionGroupBodySchema, + updatePermissionGroupBodySchema, +} from '@/lib/api/contracts/permission-groups' + +describe('createPermissionGroupBodySchema', () => { + it('accepts a group that defaults to all workspaces', () => { + const result = createPermissionGroupBodySchema.safeParse({ name: 'Engineering' }) + expect(result.success).toBe(true) + }) + + it('accepts a specific-scope group with at least one workspace', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Contractors', + appliesToAllWorkspaces: false, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(true) + }) + + it('rejects a specific-scope group with no workspaces', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Contractors', + appliesToAllWorkspaces: false, + workspaceIds: [], + }) + expect(result.success).toBe(false) + }) + + it('rejects a specific-scope group that omits workspaceIds', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Contractors', + appliesToAllWorkspaces: false, + }) + expect(result.success).toBe(false) + }) + + it('rejects a default group that targets specific workspaces', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Baseline', + isDefault: true, + appliesToAllWorkspaces: false, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) + + it('accepts a default group that applies to all workspaces', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Baseline', + isDefault: true, + appliesToAllWorkspaces: true, + }) + expect(result.success).toBe(true) + }) + + it('rejects a default group with workspaceIds (appliesToAllWorkspaces omitted)', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Baseline', + isDefault: true, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) + + it('rejects an all-workspaces group that also names specific workspaces', () => { + const result = createPermissionGroupBodySchema.safeParse({ + name: 'Engineering', + appliesToAllWorkspaces: true, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) +}) + +describe('updatePermissionGroupBodySchema', () => { + it('accepts an empty update', () => { + expect(updatePermissionGroupBodySchema.safeParse({}).success).toBe(true) + }) + + it('rejects switching to specific scope with no workspaces', () => { + const result = updatePermissionGroupBodySchema.safeParse({ + appliesToAllWorkspaces: false, + workspaceIds: [], + }) + expect(result.success).toBe(false) + }) + + it('accepts switching to specific scope with workspaces', () => { + const result = updatePermissionGroupBodySchema.safeParse({ + appliesToAllWorkspaces: false, + workspaceIds: ['ws-1', 'ws-2'], + }) + expect(result.success).toBe(true) + }) + + it('rejects making a specific-scope group the default', () => { + const result = updatePermissionGroupBodySchema.safeParse({ + isDefault: true, + appliesToAllWorkspaces: false, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) + + it('rejects workspaceIds when making the group the default', () => { + const result = updatePermissionGroupBodySchema.safeParse({ + isDefault: true, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) + + it('rejects workspaceIds on an all-workspaces update', () => { + const result = updatePermissionGroupBodySchema.safeParse({ + appliesToAllWorkspaces: true, + workspaceIds: ['ws-1'], + }) + expect(result.success).toBe(false) + }) +}) diff --git a/apps/sim/lib/api/contracts/permission-groups.ts b/apps/sim/lib/api/contracts/permission-groups.ts index 8e471952494..2ea7d651ff2 100644 --- a/apps/sim/lib/api/contracts/permission-groups.ts +++ b/apps/sim/lib/api/contracts/permission-groups.ts @@ -43,6 +43,13 @@ export const permissionGroupDetailParamsSchema = z.object({ groupId: z.string().min(1), }) +/** A workspace a permission group targets (id + display name). */ +export const permissionGroupWorkspaceRefSchema = z.object({ + id: z.string(), + name: z.string(), +}) +export type PermissionGroupWorkspaceRef = z.output + export const permissionGroupSchema = z.object({ id: z.string(), name: z.string(), @@ -55,6 +62,10 @@ export const permissionGroupSchema = z.object({ creatorEmail: z.string().nullable(), memberCount: z.number(), isDefault: z.boolean(), + /** When true the group governs every workspace; when false only `workspaces`. */ + appliesToAllWorkspaces: z.boolean(), + /** Workspaces targeted when `appliesToAllWorkspaces` is false (empty otherwise). */ + workspaces: z.array(permissionGroupWorkspaceRefSchema), }) export type PermissionGroup = z.output @@ -68,6 +79,9 @@ export const permissionGroupWriteSchema = z.object({ createdAt: z.string(), updatedAt: z.string(), isDefault: z.boolean(), + appliesToAllWorkspaces: z.boolean(), + /** Ids of targeted workspaces when `appliesToAllWorkspaces` is false. */ + workspaceIds: z.array(z.string()), }) export type PermissionGroupWrite = z.output @@ -97,19 +111,73 @@ export const userPermissionConfigSchema = z.object({ }) export type UserPermissionConfig = z.output -export const createPermissionGroupBodySchema = z.object({ - name: z.string().trim().min(1).max(100), - description: z.string().max(500).optional(), - config: permissionGroupConfigSchema.optional(), - isDefault: z.boolean().optional(), -}) +/** Upper bound on how many workspaces a single group can explicitly target. */ +export const MAX_PERMISSION_GROUP_WORKSPACES = 500 -export const updatePermissionGroupBodySchema = z.object({ - name: z.string().trim().min(1).max(100).optional(), - description: z.string().max(500).nullable().optional(), - config: permissionGroupConfigSchema.optional(), - isDefault: z.boolean().optional(), -}) +const workspaceIdsSchema = z.array(z.string().min(1)).max(MAX_PERMISSION_GROUP_WORKSPACES) + +/** + * Enforce the workspace-scope invariants shared by create and update: + * - a specific-scope group (`appliesToAllWorkspaces === false`) must name at + * least one workspace, + * - the organization default group must apply to all workspaces, and + * - an all-workspaces or default group must not name specific workspaces + * (otherwise `workspaceIds` would be silently dropped server-side). + */ +function refineWorkspaceScope( + body: { appliesToAllWorkspaces?: boolean; workspaceIds?: string[]; isDefault?: boolean }, + ctx: z.RefinementCtx +) { + // A default group is always org-wide, and an explicit all-workspaces group has + // no specific workspaces. Reject workspaceIds in either case rather than + // silently dropping them when the scope resolves to all-workspaces. + const allWorkspaces = body.isDefault === true || body.appliesToAllWorkspaces === true + if (allWorkspaces && body.workspaceIds && body.workspaceIds.length > 0) { + ctx.addIssue({ + code: 'custom', + path: ['workspaceIds'], + message: 'workspaceIds can only be set when the group targets specific workspaces', + }) + } + if (body.appliesToAllWorkspaces === false) { + if (!body.workspaceIds || body.workspaceIds.length === 0) { + ctx.addIssue({ + code: 'custom', + path: ['workspaceIds'], + message: 'Select at least one workspace when the group targets specific workspaces', + }) + } + if (body.isDefault === true) { + ctx.addIssue({ + code: 'custom', + path: ['appliesToAllWorkspaces'], + message: 'The default group must apply to all workspaces', + }) + } + } +} + +export const createPermissionGroupBodySchema = z + .object({ + name: z.string().trim().min(1).max(100), + description: z.string().max(500).optional(), + config: permissionGroupConfigSchema.optional(), + isDefault: z.boolean().optional(), + appliesToAllWorkspaces: z.boolean().optional(), + workspaceIds: workspaceIdsSchema.optional(), + }) + .superRefine(refineWorkspaceScope) + +export const updatePermissionGroupBodySchema = z + .object({ + name: z.string().trim().min(1).max(100).optional(), + description: z.string().max(500).nullable().optional(), + config: permissionGroupConfigSchema.optional(), + isDefault: z.boolean().optional(), + appliesToAllWorkspaces: z.boolean().optional(), + workspaceIds: workspaceIdsSchema.optional(), + }) + .superRefine(refineWorkspaceScope) export const removePermissionGroupMemberQuerySchema = z.object({ memberId: z.string().min(1), @@ -234,7 +302,26 @@ export const bulkAddPermissionGroupMembersContract = defineRouteContract({ mode: 'json', schema: z.object({ added: z.number(), - moved: z.number(), + // Users not added because they were already in this group. A conflicting + // selection fails the whole request (409) rather than being skipped, so + // the add is all-or-nothing for conflicts. + skipped: z.number(), + }), + }, +}) + +/** + * List the workspaces belonging to an organization, used to populate the + * workspace multi-select when scoping a permission group to specific workspaces. + */ +export const listOrganizationWorkspacesContract = defineRouteContract({ + method: 'GET', + path: '/api/organizations/[id]/workspaces', + params: permissionGroupParamsSchema, + response: { + mode: 'json', + schema: z.object({ + workspaces: z.array(permissionGroupWorkspaceRefSchema), }), }, }) diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 22d8e205302..2853a82a2d3 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -7,7 +7,10 @@ export const PERMISSION_GROUP_CONSTRAINTS = { export const PERMISSION_GROUP_MEMBER_CONSTRAINTS = { groupUser: 'permission_group_member_group_user_unique', - organizationUser: 'permission_group_member_organization_user_unique', +} as const + +export const PERMISSION_GROUP_WORKSPACE_CONSTRAINTS = { + groupWorkspace: 'permission_group_workspace_group_workspace_unique', } as const export const permissionGroupConfigSchema = z.object({ diff --git a/packages/db/migrations/0238_workspace_scoped_permission_groups.sql b/packages/db/migrations/0238_workspace_scoped_permission_groups.sql new file mode 100644 index 00000000000..87b4dde2ce3 --- /dev/null +++ b/packages/db/migrations/0238_workspace_scoped_permission_groups.sql @@ -0,0 +1,34 @@ +-- Replay-safety: this file ends in CONCURRENTLY index ops below an embedded COMMIT, +-- so a failure there replays the whole file from the top — every statement here is idempotent. +CREATE TABLE IF NOT EXISTS "permission_group_workspace" ( + "id" text PRIMARY KEY NOT NULL, + "permission_group_id" text NOT NULL, + "workspace_id" text NOT NULL, + "organization_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "permission_group" ADD COLUMN IF NOT EXISTS "applies_to_all_workspaces" boolean DEFAULT true NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "permission_group_workspace" ADD CONSTRAINT "permission_group_workspace_permission_group_id_permission_group_id_fk" FOREIGN KEY ("permission_group_id") REFERENCES "public"."permission_group"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "permission_group_workspace" ADD CONSTRAINT "permission_group_workspace_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "permission_group_workspace" ADD CONSTRAINT "permission_group_workspace_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION WHEN duplicate_object THEN null; +END $$;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "permission_group_workspace_group_id_idx" ON "permission_group_workspace" USING btree ("permission_group_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "permission_group_workspace_workspace_id_idx" ON "permission_group_workspace" USING btree ("workspace_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "permission_group_workspace_group_workspace_unique" ON "permission_group_workspace" USING btree ("permission_group_id","workspace_id");--> statement-breakpoint +-- permission_group_member is an existing table: swap its (organization_id, user_id) +-- index CONCURRENTLY so the build/drop never write-locks the relation (runner +-- convention — plain CREATE/DROP INDEX takes ACCESS EXCLUSIVE for the whole op). +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "permission_group_member_organization_user_idx" ON "permission_group_member" USING btree ("organization_id","user_id");--> statement-breakpoint +DROP INDEX CONCURRENTLY IF EXISTS "permission_group_member_organization_user_unique";--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/migrations/meta/0238_snapshot.json b/packages/db/migrations/meta/0238_snapshot.json new file mode 100644 index 00000000000..f669541d81d --- /dev/null +++ b/packages/db/migrations/meta/0238_snapshot.json @@ -0,0 +1,16584 @@ +{ + "id": "ec49405c-a007-4cf2-9706-ddb27a78ef19", + "prevId": "52122264-d797-4980-bf94-301ea12da917", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_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_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "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_group_id_idx": { + "name": "permission_group_workspace_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_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_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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1ebad533fb8..b7ce43b7f77 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1660,6 +1660,13 @@ "when": 1781398805524, "tag": "0237_user_settings_timezone", "breakpoints": true + }, + { + "idx": 238, + "version": "7", + "when": 1781554278388, + "tag": "0238_workspace_scoped_permission_groups", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 96fd728c1c1..7a2c3e4d0b9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2940,6 +2940,7 @@ export const permissionGroup = pgTable( createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), isDefault: boolean('is_default').notNull().default(false), + appliesToAllWorkspaces: boolean('applies_to_all_workspaces').notNull().default(true), }, (table) => ({ createdByIdx: index('permission_group_created_by_idx').on(table.createdBy), @@ -2953,6 +2954,38 @@ export const permissionGroup = pgTable( }) ) +/** + * Workspaces a `permission_group` targets when `applies_to_all_workspaces` is + * false. Rows are absent for organization-wide groups. A group with zero rows + * while `applies_to_all_workspaces = false` governs no workspace. + */ +export const permissionGroupWorkspace = pgTable( + 'permission_group_workspace', + { + id: text('id').primaryKey(), + permissionGroupId: text('permission_group_id') + .notNull() + .references(() => permissionGroup.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + organizationId: text('organization_id') + .notNull() + .references(() => organization.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + }, + (table) => ({ + permissionGroupIdIdx: index('permission_group_workspace_group_id_idx').on( + table.permissionGroupId + ), + workspaceIdIdx: index('permission_group_workspace_workspace_id_idx').on(table.workspaceId), + groupWorkspaceUnique: uniqueIndex('permission_group_workspace_group_workspace_unique').on( + table.permissionGroupId, + table.workspaceId + ), + }) +) + export const permissionGroupMember = pgTable( 'permission_group_member', { @@ -2975,7 +3008,7 @@ export const permissionGroupMember = pgTable( table.permissionGroupId, table.userId ), - organizationUserUnique: uniqueIndex('permission_group_member_organization_user_unique').on( + organizationUserIdx: index('permission_group_member_organization_user_idx').on( table.organizationId, table.userId ), diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index b2bfbb42513..fb72acb5b1c 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1025,6 +1025,14 @@ export const schemaMock = { createdAt: 'createdAt', updatedAt: 'updatedAt', isDefault: 'isDefault', + appliesToAllWorkspaces: 'appliesToAllWorkspaces', + }, + permissionGroupWorkspace: { + id: 'id', + permissionGroupId: 'permissionGroupId', + workspaceId: 'workspaceId', + organizationId: 'organizationId', + createdAt: 'createdAt', }, permissionGroupMember: { id: 'id', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index bbef78867f8..cee05f4d2b1 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 851, - zodRoutes: 851, + totalRoutes: 852, + zodRoutes: 852, nonZodRoutes: 0, } as const From 91666b585f7c571a0f2c58c159a77918136871a8 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:00:08 -0700 Subject: [PATCH 23/24] feat(scheduled-tasks): migrate jobs agent to scheduled tasks agent (#5090) --- .../app/api/copilot/chat/resources/route.ts | 5 +- .../chat-context-kind-registry.tsx | 5 + .../home/components/message-content/utils.ts | 1 + .../add-resource-dropdown.tsx | 14 + .../resource-content/resource-content.tsx | 150 +++++ .../resource-registry/resource-registry.tsx | 14 + .../components/chip-clipboard-codec.ts | 3 + .../user-input/components/constants.ts | 1 + .../home/hooks/stream/stream-helpers.ts | 32 +- .../[workspaceId]/home/hooks/use-chat.ts | 2 + .../app/workspace/[workspaceId]/home/types.ts | 7 +- apps/sim/background/schedule-execution.ts | 10 +- apps/sim/lib/api/contracts/copilot.ts | 1 + .../lib/copilot/generated/tool-catalog-v1.ts | 292 +++++---- .../lib/copilot/generated/tool-schemas-v1.ts | 214 +++--- .../lib/copilot/resources/extraction.test.ts | 64 +- apps/sim/lib/copilot/resources/extraction.ts | 22 + apps/sim/lib/copilot/resources/persistence.ts | 8 +- apps/sim/lib/copilot/resources/types.ts | 13 + .../tool-executor/register-handlers.ts | 12 +- .../lib/copilot/tools/handlers/resources.ts | 23 + apps/sim/lib/copilot/tools/mcp/definitions.ts | 6 +- .../copilot/tools/server/jobs/get-job-logs.ts | 4 +- apps/sim/stores/panel/types.ts | 1 + bun.lock | 617 ++++++++++-------- 25 files changed, 948 insertions(+), 573 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/resources/route.ts b/apps/sim/app/api/copilot/chat/resources/route.ts index 5417fbe4a49..823b9a94aba 100644 --- a/apps/sim/app/api/copilot/chat/resources/route.ts +++ b/apps/sim/app/api/copilot/chat/resources/route.ts @@ -17,6 +17,7 @@ import { createUnauthorizedResponse, } from '@/lib/copilot/request/http' import type { ChatResource, ResourceType } from '@/lib/copilot/resources/persistence' +import { GENERIC_RESOURCE_TITLES } from '@/lib/copilot/resources/types' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('CopilotChatResourcesAPI') @@ -27,10 +28,10 @@ const VALID_RESOURCE_TYPES = new Set([ 'workflow', 'knowledgebase', 'folder', + 'scheduledtask', 'log', 'integration', ]) -const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log']) export const POST = withRouteHandler(async (req: NextRequest) => { try { @@ -76,7 +77,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { let merged: ChatResource[] if (prev) { - if (GENERIC_TITLES.has(prev.title) && !GENERIC_TITLES.has(resource.title)) { + if (GENERIC_RESOURCE_TITLES.has(prev.title) && !GENERIC_RESOURCE_TITLES.has(resource.title)) { merged = existing.map((r) => `${r.type}:${r.id}` === key ? { ...r, title: resource.title } : r ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx index 2269283392b..e01cbc5f80d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import { + Calendar, Database, Folder as FolderIcon, Library, @@ -79,6 +80,10 @@ export const CHAT_CONTEXT_KIND_REGISTRY: Record , }, + scheduledtask: { + label: 'Scheduled task', + renderIcon: ({ className }) => , + }, past_chat: { label: 'Past chat', renderIcon: ({ className }) => , diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index 5bcc8b1aa4c..b5e05c3e794 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -51,6 +51,7 @@ const TOOL_ICONS: Record = { knowledge: Database, knowledge_base: Database, table: TableIcon, + scheduled_task: Calendar, job: Calendar, agent: AgentIcon, custom_tool: Wrench, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 889ac5ada6b..2f3283aa245 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -1,6 +1,7 @@ 'use client' import { useMemo, useState } from 'react' +import { truncate } from '@sim/utils/string' import { Button, DropdownMenu, @@ -30,6 +31,7 @@ import { useFolders } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useLogsList } from '@/hooks/queries/logs' import { useMothershipChats } from '@/hooks/queries/mothership-chats' +import { useWorkspaceSchedules } from '@/hooks/queries/schedules' import { useTablesList } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders' @@ -77,6 +79,7 @@ export function useAvailableResources( const { data: folders = [] } = useFolders(workspaceId) const { data: fileFolders = [] } = useWorkspaceFileFolders(workspaceId) const { data: tasks = [] } = useMothershipChats(workspaceId) + const { data: schedules = [] } = useWorkspaceSchedules(workspaceId) const { data: logsData } = useLogsList(workspaceId, LOG_DROPDOWN_FILTERS) const logs = useMemo(() => (logsData?.pages ?? []).flatMap((page) => page.logs), [logsData]) @@ -155,6 +158,16 @@ export function useAvailableResources( isOpen: existingKeys.has(`task:${t.id}`), })), }, + { + type: 'scheduledtask' as const, + items: schedules + .filter((s) => s.sourceType === 'job') + .map((s) => ({ + id: s.id, + name: s.jobTitle || truncate(s.prompt ?? '', 40) || 'Scheduled Task', + isOpen: existingKeys.has(`scheduledtask:${s.id}`), + })), + }, { type: 'log' as const, items: logs.map((log) => { @@ -179,6 +192,7 @@ export function useAvailableResources( files, knowledgeBases, tasks, + schedules, logs, existingKeys, excludeTypes, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 965987807b8..55219f75006 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -2,9 +2,11 @@ import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react' import { createLogger } from '@sim/logger' +import { format } from 'date-fns' import { useRouter } from 'next/navigation' import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn' import { + Calendar, Download, FileX, Folder as FolderIcon, @@ -24,6 +26,7 @@ import { import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { triggerFileDownload } from '@/lib/uploads/client/download' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' +import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' import { FileViewer, type PreviewMode, @@ -50,6 +53,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import { useFolders } from '@/hooks/queries/folders' import { useLogDetail } from '@/hooks/queries/logs' +import { useWorkspaceSchedules } from '@/hooks/queries/schedules' import { downloadTableExport } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' @@ -182,6 +186,15 @@ export const ResourceContent = memo(function ResourceContent({ case 'folder': return + case 'scheduledtask': + return ( + + ) + case 'log': return ( + case 'scheduledtask': + return case 'folder': case 'generic': return null @@ -647,6 +662,141 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) { ) } +const SCHEDULE_STATUS_LABEL: Record = { + active: 'Active', + disabled: 'Paused', + completed: 'Completed', +} + +function formatScheduleInstant(iso: string | null): string { + if (!iso) return '—' + const date = new Date(iso) + return Number.isNaN(date.getTime()) ? '—' : format(date, "EEE, MMM d 'at' h:mm a") +} + +interface ScheduledTaskFieldProps { + title: string + value: string +} + +function ScheduledTaskField({ title, value }: ScheduledTaskFieldProps) { + return ( +
+ {title} + {value} +
+ ) +} + +interface EmbeddedScheduledTaskProps { + workspaceId: string + scheduleId: string +} + +function EmbeddedScheduledTask({ workspaceId, scheduleId }: EmbeddedScheduledTaskProps) { + const { data: schedules = [], isLoading, isError } = useWorkspaceSchedules(workspaceId) + const schedule = useMemo( + () => schedules.find((s) => s.id === scheduleId), + [schedules, scheduleId] + ) + + if (isLoading && !schedule) return LOADING_SKELETON + + if (!schedule) { + const heading = isError ? "Couldn't load scheduled task" : 'Scheduled task not found' + const detail = isError + ? 'Something went wrong loading this scheduled task. Try again.' + : 'This scheduled task may have been deleted' + return ( +
+ +
+

{heading}

+

{detail}

+
+
+ ) + } + + const title = schedule.jobTitle || schedule.prompt || 'Scheduled task' + const timing = schedule.cronExpression + ? parseCronToHumanReadable(schedule.cronExpression, schedule.timezone) + : 'Runs once' + const status = SCHEDULE_STATUS_LABEL[schedule.status] ?? schedule.status + + return ( +
+
+ +

{title}

+
+ +
+ + + + +
+ +
+ Prompt +

+ {schedule.prompt || '—'} +

+
+ + {schedule.jobHistory && schedule.jobHistory.length > 0 && ( +
+ Recent runs +
+ {schedule.jobHistory.slice(0, 5).map((run, index) => ( +
+ + {formatScheduleInstant(run.timestamp)} + + {run.summary} +
+ ))} +
+
+ )} +
+ ) +} + +interface EmbeddedScheduledTaskActionsProps { + workspaceId: string +} + +function EmbeddedScheduledTaskActions({ workspaceId }: EmbeddedScheduledTaskActionsProps) { + const router = useRouter() + + const handleOpenScheduledTasks = () => { + router.push(`/workspace/${workspaceId}/scheduled-tasks`) + } + + return ( + + + + + +

Open in scheduled tasks

+
+
+ ) +} + interface EmbeddedLogProps { workspaceId: string logId: string diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index d41ba2a8770..3d0df4475f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -3,6 +3,7 @@ import type { ElementType, ReactNode } from 'react' import type { QueryClient } from '@tanstack/react-query' import { + Calendar, Connections, Database, File as FileIcon, @@ -23,6 +24,7 @@ import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color' import { knowledgeKeys } from '@/hooks/queries/kb/knowledge' import { logKeys } from '@/hooks/queries/logs' import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' +import { scheduleKeys } from '@/hooks/queries/schedules' import { tableKeys } from '@/hooks/queries/tables' import { folderKeys } from '@/hooks/queries/utils/folder-keys' import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists' @@ -183,6 +185,15 @@ export const RESOURCE_REGISTRY: Record , }, + scheduledtask: { + type: 'scheduledtask', + label: 'Scheduled Tasks', + icon: Calendar, + renderTabIcon: (_resource, className) => ( + + ), + renderDropdownItem: (props) => , + }, log: { type: 'log', label: 'Logs', @@ -241,6 +252,9 @@ const RESOURCE_INVALIDATORS: Record< task: (qc, wId) => { qc.invalidateQueries({ queryKey: mothershipChatKeys.list(wId) }) }, + scheduledtask: (qc, wId) => { + qc.invalidateQueries({ queryKey: scheduleKeys.list(wId) }) + }, log: (qc, _wId, id) => { qc.invalidateQueries({ queryKey: logKeys.details() }) qc.invalidateQueries({ queryKey: logKeys.detail(id) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts index 443c1222892..43cdbef772a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts @@ -27,6 +27,7 @@ const PORTABLE_KIND_TO_ID_FIELD = { file: 'fileId', folder: 'folderId', filefolder: 'fileFolderId', + scheduledtask: 'scheduleId', knowledge: 'knowledgeId', past_chat: 'chatId', workflow: 'workflowId', @@ -207,6 +208,8 @@ export function chipLinkToContext(link: ParsedChipLink): ChatContext { return { kind: 'folder', folderId: link.id, label: link.label } case 'filefolder': return { kind: 'filefolder', fileFolderId: link.id, label: link.label } + case 'scheduledtask': + return { kind: 'scheduledtask', scheduleId: link.id, label: link.label } case 'knowledge': return { kind: 'knowledge', knowledgeId: link.id, label: link.label } case 'past_chat': diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts index a1065032200..645ace30c60 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts @@ -112,6 +112,7 @@ const RESOURCE_TO_CONTEXT: Record< task: (r) => ({ kind: 'past_chat', chatId: r.id, label: r.title }), log: (r) => ({ kind: 'logs', executionId: r.id, label: r.title }), integration: (r) => ({ kind: 'integration', blockType: r.id, label: r.title }), + scheduledtask: (r) => ({ kind: 'scheduledtask', scheduleId: r.id, label: r.title }), generic: (r) => ({ kind: 'docs', label: r.title }), } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 2c438e0c858..9209614ec4f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -18,10 +18,10 @@ import { ManageCredentialOperation, ManageCustomTool, ManageCustomToolOperation, - ManageJob, - ManageJobOperation, ManageMcpTool, ManageMcpToolOperation, + ManageScheduledTask, + ManageScheduledTaskOperation, ManageSkill, ManageSkillOperation, MoveFolder, @@ -345,17 +345,17 @@ export function resolveToolDisplayTitle( ) } - if (name === ManageJob.id) { + if (name === ManageScheduledTask.id) { return resolveOperationDisplayTitle( args.operation, { - [ManageJobOperation.create]: 'Creating job', - [ManageJobOperation.get]: 'Getting job', - [ManageJobOperation.update]: 'Updating job', - [ManageJobOperation.delete]: 'Deleting job', - [ManageJobOperation.list]: 'Listing jobs', + [ManageScheduledTaskOperation.create]: 'Creating scheduled task', + [ManageScheduledTaskOperation.get]: 'Getting scheduled task', + [ManageScheduledTaskOperation.update]: 'Updating scheduled task', + [ManageScheduledTaskOperation.delete]: 'Deleting scheduled task', + [ManageScheduledTaskOperation.list]: 'Listing scheduled tasks', }, - 'Job action' + 'Scheduled task action' ) } @@ -496,17 +496,17 @@ export function resolveStreamingToolDisplayTitle( ) } - if (name === ManageJob.id) { + if (name === ManageScheduledTask.id) { return resolveOperationDisplayTitle( matchStreamingStringArg(streamingArgs, 'operation'), { - [ManageJobOperation.create]: 'Creating job', - [ManageJobOperation.get]: 'Getting job', - [ManageJobOperation.update]: 'Updating job', - [ManageJobOperation.delete]: 'Deleting job', - [ManageJobOperation.list]: 'Listing jobs', + [ManageScheduledTaskOperation.create]: 'Creating scheduled task', + [ManageScheduledTaskOperation.get]: 'Getting scheduled task', + [ManageScheduledTaskOperation.update]: 'Updating scheduled task', + [ManageScheduledTaskOperation.delete]: 'Deleting scheduled task', + [ManageScheduledTaskOperation.list]: 'Listing scheduled tasks', }, - 'Job action' + 'Scheduled task action' ) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 5374c77b416..dc5ae5d86d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -294,6 +294,8 @@ function isChatContext(value: unknown): value is ChatContext { return typeof value.folderId === 'string' case 'filefolder': return typeof value.fileFolderId === 'string' + case 'scheduledtask': + return typeof value.scheduleId === 'string' case 'docs': return true case 'slash_command': diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 30307e4ca31..531a5a18639 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -12,7 +12,6 @@ import { GetPageContents, Glob, Grep, - Job, Knowledge, KnowledgeBase, ManageMcpTool, @@ -21,6 +20,7 @@ import { OpenResource, Read as ReadTool, Research, + ScheduledTask, ScrapePage, SearchLibraryDocs, SearchOnline, @@ -193,6 +193,8 @@ export const SUBAGENT_LABELS: Record = { superagent: 'Superagent', run: 'Run Agent', agent: 'Tools Agent', + scheduled_task: 'Scheduled Task Agent', + // `job` retained as a backward-compat alias so historical transcripts still render a label. job: 'Job Agent', file: 'File Agent', media: 'Media Agent', @@ -230,7 +232,8 @@ export const TOOL_UI_METADATA: Record = { [Knowledge.id]: { title: 'Knowledge Agent' }, [KnowledgeBase.id]: { title: 'Managing knowledge base' }, [Table.id]: { title: 'Table Agent' }, - [Job.id]: { title: 'Job Agent' }, + [ScheduledTask.id]: { title: 'Scheduled Task Agent' }, + job: { title: 'Job Agent' }, [Agent.id]: { title: 'Tools Agent' }, custom_tool: { title: 'Creating tool' }, [Research.id]: { title: 'Research Agent' }, diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 7ab08bfbc38..07cc2f40aa0 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -1036,17 +1036,17 @@ function buildJobPrompt(jobRecord: { } parts.push('') parts.push( - 'Use this history to avoid duplicate work. After completing meaningful work this run, call update_job_history to record what you did.' + 'Use this history to avoid duplicate work. After completing meaningful work this run, call update_scheduled_task_history to record what you did.' ) } else if (jobRecord.runCount > 0) { parts.push('') parts.push( - 'No previous run history recorded. After completing meaningful work, call update_job_history to record what you did for future runs.' + 'No previous run history recorded. After completing meaningful work, call update_scheduled_task_history to record what you did for future runs.' ) } else { parts.push('') parts.push( - 'This is the first run. After completing meaningful work, call update_job_history to record what you did so future runs have context.' + 'This is the first run. After completing meaningful work, call update_scheduled_task_history to record what you did so future runs have context.' ) } @@ -1055,7 +1055,7 @@ function buildJobPrompt(jobRecord: { parts.push('COMPLETION PROTOCOL:') parts.push('This is a poll-until-done job. After executing the task above:') parts.push( - `- If the success condition is met, take the required action, then call complete_job(jobId: "${jobRecord.id}") to stop the job.` + `- If the success condition is met, take the required action, then call complete_scheduled_task(jobId: "${jobRecord.id}") to stop the scheduled task.` ) parts.push( '- If the success condition is NOT met, do nothing extra. The job will run again on schedule.' @@ -1285,7 +1285,7 @@ export async function executeJobInline(payload: JobExecutionPayload) { try { responseBody = await response.json() const toolCalls = responseBody?.toolCalls as Array<{ name?: string }> | undefined - wasCompletedByTool = toolCalls?.some((tc) => tc.name === 'complete_job') ?? false + wasCompletedByTool = toolCalls?.some((tc) => tc.name === 'complete_scheduled_task') ?? false } catch { if (timeoutController.isTimedOut()) { throw new Error(getTimeoutErrorMessage(null, timeoutController.timeoutMs)) diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index 3da7fdd6d01..f522728f647 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -92,6 +92,7 @@ const copilotResourceTypeSchema = z.enum([ 'workflow', 'knowledgebase', 'folder', + 'scheduledtask', 'log', ]) diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 87178f69903..ae4296db336 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -10,7 +10,7 @@ export interface ToolCatalogEntry { | 'agent' | 'auth' | 'check_deployment_status' - | 'complete_job' + | 'complete_scheduled_task' | 'crawl_website' | 'create_file' | 'create_file_folder' @@ -42,14 +42,13 @@ export interface ToolCatalogEntry { | 'get_block_upstream_references' | 'get_deployed_workflow_state' | 'get_deployment_log' - | 'get_job_logs' | 'get_page_contents' | 'get_platform_actions' + | 'get_scheduled_task_logs' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' | 'grep' - | 'job' | 'knowledge' | 'knowledge_base' | 'list_file_folders' @@ -61,8 +60,8 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' - | 'manage_job' | 'manage_mcp_tool' + | 'manage_scheduled_task' | 'manage_skill' | 'materialize_file' | 'media' @@ -88,6 +87,7 @@ export interface ToolCatalogEntry { | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' + | 'scheduled_task' | 'scrape_page' | 'search_documentation' | 'search_library_docs' @@ -99,7 +99,7 @@ export interface ToolCatalogEntry { | 'superagent' | 'table' | 'update_deployment_version' - | 'update_job_history' + | 'update_scheduled_task_history' | 'update_workspace_mcp_server' | 'user_memory' | 'user_table' @@ -111,7 +111,7 @@ export interface ToolCatalogEntry { | 'agent' | 'auth' | 'check_deployment_status' - | 'complete_job' + | 'complete_scheduled_task' | 'crawl_website' | 'create_file' | 'create_file_folder' @@ -143,14 +143,13 @@ export interface ToolCatalogEntry { | 'get_block_upstream_references' | 'get_deployed_workflow_state' | 'get_deployment_log' - | 'get_job_logs' | 'get_page_contents' | 'get_platform_actions' + | 'get_scheduled_task_logs' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' | 'grep' - | 'job' | 'knowledge' | 'knowledge_base' | 'list_file_folders' @@ -162,8 +161,8 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' - | 'manage_job' | 'manage_mcp_tool' + | 'manage_scheduled_task' | 'manage_skill' | 'materialize_file' | 'media' @@ -189,6 +188,7 @@ export interface ToolCatalogEntry { | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' + | 'scheduled_task' | 'scrape_page' | 'search_documentation' | 'search_library_docs' @@ -200,7 +200,7 @@ export interface ToolCatalogEntry { | 'superagent' | 'table' | 'update_deployment_version' - | 'update_job_history' + | 'update_scheduled_task_history' | 'update_workspace_mcp_server' | 'user_memory' | 'user_table' @@ -216,11 +216,11 @@ export interface ToolCatalogEntry { | 'auth' | 'deploy' | 'file' - | 'job' | 'knowledge' | 'media' | 'research' | 'run' + | 'scheduled_task' | 'superagent' | 'table' | 'workflow' @@ -275,15 +275,15 @@ export const CheckDeploymentStatus: ToolCatalogEntry = { }, } -export const CompleteJob: ToolCatalogEntry = { - id: 'complete_job', - name: 'complete_job', +export const CompleteScheduledTask: ToolCatalogEntry = { + id: 'complete_scheduled_task', + name: 'complete_scheduled_task', route: 'sim', mode: 'async', parameters: { type: 'object', properties: { - jobId: { type: 'string', description: 'The ID of the job to mark as completed.' }, + jobId: { type: 'string', description: 'The ID of the scheduled task to mark as completed.' }, }, required: ['jobId'], }, @@ -1990,26 +1990,6 @@ export const GetDeploymentLog: ToolCatalogEntry = { }, } -export const GetJobLogs: ToolCatalogEntry = { - id: 'get_job_logs', - name: 'get_job_logs', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - executionId: { type: 'string', description: 'Optional execution ID for a specific run.' }, - includeDetails: { - type: 'boolean', - description: 'Include tool calls, outputs, and cost details.', - }, - jobId: { type: 'string', description: 'The job (schedule) ID to get logs for.' }, - limit: { type: 'number', description: 'Max number of entries (default: 3, max: 5)' }, - }, - required: ['jobId'], - }, -} - export const GetPageContents: ToolCatalogEntry = { id: 'get_page_contents', name: 'get_page_contents', @@ -2045,6 +2025,26 @@ export const GetPlatformActions: ToolCatalogEntry = { parameters: { type: 'object', properties: {} }, } +export const GetScheduledTaskLogs: ToolCatalogEntry = { + id: 'get_scheduled_task_logs', + name: 'get_scheduled_task_logs', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + executionId: { type: 'string', description: 'Optional execution ID for a specific run.' }, + includeDetails: { + type: 'boolean', + description: 'Include tool calls, outputs, and cost details.', + }, + jobId: { type: 'string', description: 'The scheduled task (schedule) ID to get logs for.' }, + limit: { type: 'number', description: 'Max number of entries (default: 3, max: 5)' }, + }, + required: ['jobId'], + }, +} + export const GetWorkflowData: ToolCatalogEntry = { id: 'get_workflow_data', name: 'get_workflow_data', @@ -2155,20 +2155,6 @@ export const Grep: ToolCatalogEntry = { }, } -export const Job: ToolCatalogEntry = { - id: 'job', - name: 'job', - route: 'subagent', - mode: 'async', - parameters: { - properties: { request: { description: 'What job action is needed.', type: 'string' } }, - required: ['request'], - type: 'object', - }, - subagentId: 'job', - internal: true, -} - export const Knowledge: ToolCatalogEntry = { id: 'knowledge', name: 'knowledge', @@ -2522,7 +2508,7 @@ export const ManageCustomTool: ToolCatalogEntry = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, schema: { @@ -2576,9 +2562,61 @@ export const ManageCustomTool: ToolCatalogEntry = { requiredPermission: 'write', } -export const ManageJob: ToolCatalogEntry = { - id: 'manage_job', - name: 'manage_job', +export const ManageMcpTool: ToolCatalogEntry = { + id: 'manage_mcp_tool', + name: 'manage_mcp_tool', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + config: { + type: 'object', + description: 'Required for add and edit. The MCP server configuration.', + properties: { + enabled: { + type: 'boolean', + description: 'Whether the server is enabled (default: true)', + }, + headers: { + type: 'object', + description: 'Optional HTTP headers to send with requests (key-value pairs)', + }, + name: { type: 'string', description: 'Display name for the MCP server' }, + timeout: { + type: 'number', + description: 'Request timeout in milliseconds (default: 30000)', + }, + transport: { + type: 'string', + description: "Transport protocol: 'streamable-http' or 'sse'", + enum: ['streamable-http', 'sse'], + default: 'streamable-http', + }, + url: { type: 'string', description: 'The MCP server endpoint URL (required for add)' }, + }, + }, + operation: { + type: 'string', + description: + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + enum: ['add', 'edit', 'delete', 'list'], + }, + serverId: { + type: 'string', + description: + "The MCP server's id — the `id` field inside the VFS file agent/mcp-servers/{name}.json (the {name} filename is the display name, not the id). Required for edit and delete; omit for add and list.", + }, + }, + required: ['operation'], + }, + requiresConfirmation: true, + requiredPermission: 'write', +} + +export const ManageScheduledTask: ToolCatalogEntry = { + id: 'manage_scheduled_task', + name: 'manage_scheduled_task', route: 'sim', mode: 'async', parameters: { @@ -2592,39 +2630,42 @@ export const ManageJob: ToolCatalogEntry = { cron: { type: 'string', description: - "Cron expression for a recurring job (e.g. '0 9 * * *'). Set exactly one of cron or time: recurring -> cron; one-time -> time.", + "Cron expression for a recurring scheduled task (e.g. '0 9 * * *'). Set exactly one of cron or time: recurring -> cron; one-time -> time.", }, - jobId: { type: 'string', description: 'Job ID (required for get, update)' }, + jobId: { type: 'string', description: 'Scheduled task ID (required for get, update)' }, jobIds: { type: 'array', - description: 'Array of job IDs (for batch delete)', + description: 'Array of scheduled task IDs (for batch delete)', items: { type: 'string' }, }, lifecycle: { type: 'string', description: - "'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called.", + "'persistent' (default) or 'until_complete'. Until_complete scheduled tasks stop when complete_scheduled_task is called.", enum: ['persistent', 'until_complete'], }, maxRuns: { type: 'integer', description: 'Max executions before auto-completing. Safety limit.', }, - prompt: { type: 'string', description: 'The prompt to execute when the job fires' }, + prompt: { + type: 'string', + description: 'The prompt to execute when the scheduled task fires', + }, status: { type: 'string', - description: 'Job status: active, paused', + description: 'Scheduled task status: active, paused', enum: ['active', 'paused'], }, successCondition: { type: 'string', description: - 'What must happen for the job to be considered complete (until_complete lifecycle).', + 'What must happen for the scheduled task to be considered complete (until_complete lifecycle).', }, time: { type: 'string', description: - "ISO 8601 datetime. One-time job -> set time and omit cron. May also anchor a recurring cron job's first-fire time.", + "ISO 8601 datetime. One-time scheduled task -> set time and omit cron. May also anchor a recurring cron task's first-fire time.", }, timezone: { type: 'string', @@ -2632,7 +2673,7 @@ export const ManageJob: ToolCatalogEntry = { }, title: { type: 'string', - description: "Short descriptive title for the job (e.g. 'Email Poller')", + description: "Short descriptive title for the scheduled task (e.g. 'Email Poller')", }, }, }, @@ -2647,58 +2688,6 @@ export const ManageJob: ToolCatalogEntry = { }, } -export const ManageMcpTool: ToolCatalogEntry = { - id: 'manage_mcp_tool', - name: 'manage_mcp_tool', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - config: { - type: 'object', - description: 'Required for add and edit. The MCP server configuration.', - properties: { - enabled: { - type: 'boolean', - description: 'Whether the server is enabled (default: true)', - }, - headers: { - type: 'object', - description: 'Optional HTTP headers to send with requests (key-value pairs)', - }, - name: { type: 'string', description: 'Display name for the MCP server' }, - timeout: { - type: 'number', - description: 'Request timeout in milliseconds (default: 30000)', - }, - transport: { - type: 'string', - description: "Transport protocol: 'streamable-http' or 'sse'", - enum: ['streamable-http', 'sse'], - default: 'streamable-http', - }, - url: { type: 'string', description: 'The MCP server endpoint URL (required for add)' }, - }, - }, - operation: { - type: 'string', - description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", - enum: ['add', 'edit', 'delete', 'list'], - }, - serverId: { - type: 'string', - description: - "The MCP server's id — the `id` field inside the VFS file agent/mcp-servers/{name}.json (the {name} filename is the display name, not the id). Required for edit and delete; omit for add and list.", - }, - }, - required: ['operation'], - }, - requiresConfirmation: true, - requiredPermission: 'write', -} - export const ManageSkill: ToolCatalogEntry = { id: 'manage_skill', name: 'manage_skill', @@ -2723,7 +2712,7 @@ export const ManageSkill: ToolCatalogEntry = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, skillId: { @@ -2935,7 +2924,7 @@ export const OpenResource: ToolCatalogEntry = { type: { type: 'string', description: 'The resource type.', - enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], + enum: ['workflow', 'table', 'knowledgebase', 'file', 'log', 'scheduledtask'], }, }, required: ['type'], @@ -3484,6 +3473,22 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { requiresConfirmation: true, } +export const ScheduledTask: ToolCatalogEntry = { + id: 'scheduled_task', + name: 'scheduled_task', + route: 'subagent', + mode: 'async', + parameters: { + properties: { + request: { description: 'What scheduled task action is needed.', type: 'string' }, + }, + required: ['request'], + type: 'object', + }, + subagentId: 'scheduled_task', + internal: true, +} + export const ScrapePage: ToolCatalogEntry = { id: 'scrape_page', name: 'scrape_page', @@ -3771,15 +3776,15 @@ export const UpdateDeploymentVersion: ToolCatalogEntry = { requiredPermission: 'write', } -export const UpdateJobHistory: ToolCatalogEntry = { - id: 'update_job_history', - name: 'update_job_history', +export const UpdateScheduledTaskHistory: ToolCatalogEntry = { + id: 'update_scheduled_task_history', + name: 'update_scheduled_task_history', route: 'sim', mode: 'async', parameters: { type: 'object', properties: { - jobId: { type: 'string', description: 'The job ID.' }, + jobId: { type: 'string', description: 'The scheduled task ID.' }, summary: { type: 'string', description: @@ -4433,24 +4438,6 @@ export const ManageCustomToolOperationValues = [ ManageCustomToolOperation.list, ] as const -export const ManageJobOperation = { - create: 'create', - list: 'list', - get: 'get', - update: 'update', - delete: 'delete', -} as const - -export type ManageJobOperation = (typeof ManageJobOperation)[keyof typeof ManageJobOperation] - -export const ManageJobOperationValues = [ - ManageJobOperation.create, - ManageJobOperation.list, - ManageJobOperation.get, - ManageJobOperation.update, - ManageJobOperation.delete, -] as const - export const ManageMcpToolOperation = { add: 'add', edit: 'edit', @@ -4468,6 +4455,25 @@ export const ManageMcpToolOperationValues = [ ManageMcpToolOperation.list, ] as const +export const ManageScheduledTaskOperation = { + create: 'create', + list: 'list', + get: 'get', + update: 'update', + delete: 'delete', +} as const + +export type ManageScheduledTaskOperation = + (typeof ManageScheduledTaskOperation)[keyof typeof ManageScheduledTaskOperation] + +export const ManageScheduledTaskOperationValues = [ + ManageScheduledTaskOperation.create, + ManageScheduledTaskOperation.list, + ManageScheduledTaskOperation.get, + ManageScheduledTaskOperation.update, + ManageScheduledTaskOperation.delete, +] as const + export const ManageSkillOperation = { add: 'add', edit: 'edit', @@ -4604,7 +4610,7 @@ export const TOOL_CATALOG: Record = { [Agent.id]: Agent, [Auth.id]: Auth, [CheckDeploymentStatus.id]: CheckDeploymentStatus, - [CompleteJob.id]: CompleteJob, + [CompleteScheduledTask.id]: CompleteScheduledTask, [CrawlWebsite.id]: CrawlWebsite, [CreateFile.id]: CreateFile, [CreateFileFolder.id]: CreateFileFolder, @@ -4636,14 +4642,13 @@ export const TOOL_CATALOG: Record = { [GetBlockUpstreamReferences.id]: GetBlockUpstreamReferences, [GetDeployedWorkflowState.id]: GetDeployedWorkflowState, [GetDeploymentLog.id]: GetDeploymentLog, - [GetJobLogs.id]: GetJobLogs, [GetPageContents.id]: GetPageContents, [GetPlatformActions.id]: GetPlatformActions, + [GetScheduledTaskLogs.id]: GetScheduledTaskLogs, [GetWorkflowData.id]: GetWorkflowData, [GetWorkflowRunOptions.id]: GetWorkflowRunOptions, [Glob.id]: Glob, [Grep.id]: Grep, - [Job.id]: Job, [Knowledge.id]: Knowledge, [KnowledgeBase.id]: KnowledgeBase, [ListFileFolders.id]: ListFileFolders, @@ -4655,8 +4660,8 @@ export const TOOL_CATALOG: Record = { [LoadIntegrationTool.id]: LoadIntegrationTool, [ManageCredential.id]: ManageCredential, [ManageCustomTool.id]: ManageCustomTool, - [ManageJob.id]: ManageJob, [ManageMcpTool.id]: ManageMcpTool, + [ManageScheduledTask.id]: ManageScheduledTask, [ManageSkill.id]: ManageSkill, [MaterializeFile.id]: MaterializeFile, [Media.id]: Media, @@ -4682,6 +4687,7 @@ export const TOOL_CATALOG: Record = { [RunFromBlock.id]: RunFromBlock, [RunWorkflow.id]: RunWorkflow, [RunWorkflowUntilBlock.id]: RunWorkflowUntilBlock, + [ScheduledTask.id]: ScheduledTask, [ScrapePage.id]: ScrapePage, [SearchDocumentation.id]: SearchDocumentation, [SearchLibraryDocs.id]: SearchLibraryDocs, @@ -4693,7 +4699,7 @@ export const TOOL_CATALOG: Record = { [Superagent.id]: Superagent, [Table.id]: Table, [UpdateDeploymentVersion.id]: UpdateDeploymentVersion, - [UpdateJobHistory.id]: UpdateJobHistory, + [UpdateScheduledTaskHistory.id]: UpdateScheduledTaskHistory, [UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer, [UserMemory.id]: UserMemory, [UserTable.id]: UserTable, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index cd6421a0310..5ed8d9224d6 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -48,13 +48,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - complete_job: { + complete_scheduled_task: { parameters: { type: 'object', properties: { jobId: { type: 'string', - description: 'The ID of the job to mark as completed.', + description: 'The ID of the scheduled task to mark as completed.', }, }, required: ['jobId'], @@ -1777,31 +1777,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - get_job_logs: { - parameters: { - type: 'object', - properties: { - executionId: { - type: 'string', - description: 'Optional execution ID for a specific run.', - }, - includeDetails: { - type: 'boolean', - description: 'Include tool calls, outputs, and cost details.', - }, - jobId: { - type: 'string', - description: 'The job (schedule) ID to get logs for.', - }, - limit: { - type: 'number', - description: 'Max number of entries (default: 3, max: 5)', - }, - }, - required: ['jobId'], - }, - resultSchema: undefined, - }, get_page_contents: { parameters: { type: 'object', @@ -1837,6 +1812,31 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + get_scheduled_task_logs: { + parameters: { + type: 'object', + properties: { + executionId: { + type: 'string', + description: 'Optional execution ID for a specific run.', + }, + includeDetails: { + type: 'boolean', + description: 'Include tool calls, outputs, and cost details.', + }, + jobId: { + type: 'string', + description: 'The scheduled task (schedule) ID to get logs for.', + }, + limit: { + type: 'number', + description: 'Max number of entries (default: 3, max: 5)', + }, + }, + required: ['jobId'], + }, + resultSchema: undefined, + }, get_workflow_data: { parameters: { type: 'object', @@ -1936,19 +1936,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - job: { - parameters: { - properties: { - request: { - description: 'What job action is needed.', - type: 'string', - }, - }, - required: ['request'], - type: 'object', - }, - resultSchema: undefined, - }, knowledge: { parameters: { properties: { @@ -2290,7 +2277,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, schema: { @@ -2358,7 +2345,59 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - manage_job: { + manage_mcp_tool: { + parameters: { + type: 'object', + properties: { + config: { + type: 'object', + description: 'Required for add and edit. The MCP server configuration.', + properties: { + enabled: { + type: 'boolean', + description: 'Whether the server is enabled (default: true)', + }, + headers: { + type: 'object', + description: 'Optional HTTP headers to send with requests (key-value pairs)', + }, + name: { + type: 'string', + description: 'Display name for the MCP server', + }, + timeout: { + type: 'number', + description: 'Request timeout in milliseconds (default: 30000)', + }, + transport: { + type: 'string', + description: "Transport protocol: 'streamable-http' or 'sse'", + enum: ['streamable-http', 'sse'], + default: 'streamable-http', + }, + url: { + type: 'string', + description: 'The MCP server endpoint URL (required for add)', + }, + }, + }, + operation: { + type: 'string', + description: + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + enum: ['add', 'edit', 'delete', 'list'], + }, + serverId: { + type: 'string', + description: + "The MCP server's id — the `id` field inside the VFS file agent/mcp-servers/{name}.json (the {name} filename is the display name, not the id). Required for edit and delete; omit for add and list.", + }, + }, + required: ['operation'], + }, + resultSchema: undefined, + }, + manage_scheduled_task: { parameters: { type: 'object', properties: { @@ -2370,15 +2409,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { cron: { type: 'string', description: - "Cron expression for a recurring job (e.g. '0 9 * * *'). Set exactly one of cron or time: recurring -> cron; one-time -> time.", + "Cron expression for a recurring scheduled task (e.g. '0 9 * * *'). Set exactly one of cron or time: recurring -> cron; one-time -> time.", }, jobId: { type: 'string', - description: 'Job ID (required for get, update)', + description: 'Scheduled task ID (required for get, update)', }, jobIds: { type: 'array', - description: 'Array of job IDs (for batch delete)', + description: 'Array of scheduled task IDs (for batch delete)', items: { type: 'string', }, @@ -2386,7 +2425,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { lifecycle: { type: 'string', description: - "'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called.", + "'persistent' (default) or 'until_complete'. Until_complete scheduled tasks stop when complete_scheduled_task is called.", enum: ['persistent', 'until_complete'], }, maxRuns: { @@ -2395,22 +2434,22 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, prompt: { type: 'string', - description: 'The prompt to execute when the job fires', + description: 'The prompt to execute when the scheduled task fires', }, status: { type: 'string', - description: 'Job status: active, paused', + description: 'Scheduled task status: active, paused', enum: ['active', 'paused'], }, successCondition: { type: 'string', description: - 'What must happen for the job to be considered complete (until_complete lifecycle).', + 'What must happen for the scheduled task to be considered complete (until_complete lifecycle).', }, time: { type: 'string', description: - "ISO 8601 datetime. One-time job -> set time and omit cron. May also anchor a recurring cron job's first-fire time.", + "ISO 8601 datetime. One-time scheduled task -> set time and omit cron. May also anchor a recurring cron task's first-fire time.", }, timezone: { type: 'string', @@ -2418,7 +2457,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, title: { type: 'string', - description: "Short descriptive title for the job (e.g. 'Email Poller')", + description: "Short descriptive title for the scheduled task (e.g. 'Email Poller')", }, }, }, @@ -2433,58 +2472,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - manage_mcp_tool: { - parameters: { - type: 'object', - properties: { - config: { - type: 'object', - description: 'Required for add and edit. The MCP server configuration.', - properties: { - enabled: { - type: 'boolean', - description: 'Whether the server is enabled (default: true)', - }, - headers: { - type: 'object', - description: 'Optional HTTP headers to send with requests (key-value pairs)', - }, - name: { - type: 'string', - description: 'Display name for the MCP server', - }, - timeout: { - type: 'number', - description: 'Request timeout in milliseconds (default: 30000)', - }, - transport: { - type: 'string', - description: "Transport protocol: 'streamable-http' or 'sse'", - enum: ['streamable-http', 'sse'], - default: 'streamable-http', - }, - url: { - type: 'string', - description: 'The MCP server endpoint URL (required for add)', - }, - }, - }, - operation: { - type: 'string', - description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", - enum: ['add', 'edit', 'delete', 'list'], - }, - serverId: { - type: 'string', - description: - "The MCP server's id — the `id` field inside the VFS file agent/mcp-servers/{name}.json (the {name} filename is the display name, not the id). Required for edit and delete; omit for add and list.", - }, - }, - required: ['operation'], - }, - resultSchema: undefined, - }, manage_skill: { parameters: { type: 'object', @@ -2505,7 +2492,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_job uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, skillId: { @@ -2683,7 +2670,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: { type: 'string', description: 'The resource type.', - enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], + enum: ['workflow', 'table', 'knowledgebase', 'file', 'log', 'scheduledtask'], }, }, required: ['type'], @@ -3216,6 +3203,19 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + scheduled_task: { + parameters: { + properties: { + request: { + description: 'What scheduled task action is needed.', + type: 'string', + }, + }, + required: ['request'], + type: 'object', + }, + resultSchema: undefined, + }, scrape_page: { parameters: { type: 'object', @@ -3490,13 +3490,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - update_job_history: { + update_scheduled_task_history: { parameters: { type: 'object', properties: { jobId: { type: 'string', - description: 'The job ID.', + description: 'The scheduled task ID.', }, summary: { type: 'string', diff --git a/apps/sim/lib/copilot/resources/extraction.test.ts b/apps/sim/lib/copilot/resources/extraction.test.ts index 7244214f190..e2e381f3f31 100644 --- a/apps/sim/lib/copilot/resources/extraction.test.ts +++ b/apps/sim/lib/copilot/resources/extraction.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { extractResourcesFromToolResult } from './extraction' +import { extractDeletedResourcesFromToolResult, extractResourcesFromToolResult } from './extraction' describe('extractResourcesFromToolResult', () => { it('extracts file resources from create_file results', () => { @@ -141,4 +141,66 @@ describe('extractResourcesFromToolResult', () => { expect(resources).toEqual([]) }) + + it('auto-opens a scheduledtask resource from manage_scheduled_task create results', () => { + const resources = extractResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'create', args: { title: 'Daily Report' } }, + { jobId: 'sched_123', title: 'Daily Report', message: 'Job created successfully.' } + ) + + expect(resources).toEqual([{ type: 'scheduledtask', id: 'sched_123', title: 'Daily Report' }]) + }) + + it('auto-opens a scheduledtask resource on update, falling back to the args title', () => { + const resources = extractResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'update', args: { jobId: 'sched_123', title: 'Renamed Task' } }, + { jobId: 'sched_123', updated: ['title'], message: 'Job updated successfully' } + ) + + expect(resources).toEqual([{ type: 'scheduledtask', id: 'sched_123', title: 'Renamed Task' }]) + }) + + it('does not auto-open for read-only manage_scheduled_task operations', () => { + expect( + extractResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'list' }, + { jobs: [], count: 0 } + ) + ).toEqual([]) + expect( + extractResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'get', args: { jobId: 'sched_123' } }, + { id: 'sched_123', title: 'Daily Report' } + ) + ).toEqual([]) + }) +}) + +describe('extractDeletedResourcesFromToolResult', () => { + it('removes scheduledtask resources on manage_scheduled_task delete', () => { + const resources = extractDeletedResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'delete', args: { jobIds: ['sched_1', 'sched_2'] } }, + { deleted: ['sched_1', 'sched_2'], notFound: [] } + ) + + expect(resources).toEqual([ + { type: 'scheduledtask', id: 'sched_1', title: 'Scheduled Task' }, + { type: 'scheduledtask', id: 'sched_2', title: 'Scheduled Task' }, + ]) + }) + + it('does not remove anything for non-delete manage_scheduled_task ops', () => { + expect( + extractDeletedResourcesFromToolResult( + 'manage_scheduled_task', + { operation: 'update', args: { jobId: 'sched_1' } }, + { jobId: 'sched_1', updated: ['title'] } + ) + ).toEqual([]) + }) }) diff --git a/apps/sim/lib/copilot/resources/extraction.ts b/apps/sim/lib/copilot/resources/extraction.ts index 20e77c91183..d9e311f4720 100644 --- a/apps/sim/lib/copilot/resources/extraction.ts +++ b/apps/sim/lib/copilot/resources/extraction.ts @@ -11,6 +11,7 @@ import { GenerateVideo, Knowledge, KnowledgeBase, + ManageScheduledTask, UserTable, WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' @@ -29,6 +30,7 @@ const RESOURCE_TOOL_NAMES: Set = new Set([ FunctionExecute.id, KnowledgeBase.id, Knowledge.id, + ManageScheduledTask.id, GenerateImage.id, GenerateVideo.id, GenerateAudio.id, @@ -219,6 +221,19 @@ export function extractResourcesFromToolResult( return resources } + case ManageScheduledTask.id: { + // Read-only ops never auto-open; only create/update surface the task. + const op = getOperation(params) + if (op === 'list' || op === 'get') return [] + const jobId = (result.jobId as string) ?? (data.jobId as string) + if (jobId) { + const args = asRecord(params?.args) + const title = (result.title as string) ?? (args.title as string) ?? 'Scheduled Task' + return [{ type: 'scheduledtask', id: jobId, title }] + } + return [] + } + default: return [] } @@ -229,6 +244,7 @@ const DELETE_CAPABLE_TOOL_RESOURCE_TYPE: Record = { [WorkspaceFile.id]: 'file', [UserTable.id]: 'table', [KnowledgeBase.id]: 'knowledgebase', + [ManageScheduledTask.id]: 'scheduledtask', } export function hasDeleteCapability(toolName: string): boolean { @@ -292,6 +308,12 @@ export function extractDeletedResourcesFromToolResult( return [] } + case ManageScheduledTask.id: { + if (operation !== 'delete') return [] + const deletedIds = Array.isArray(result.deleted) ? (result.deleted as string[]) : [] + return deletedIds.map((id) => ({ type: resourceType, id, title: 'Scheduled Task' })) + } + default: return [] } diff --git a/apps/sim/lib/copilot/resources/persistence.ts b/apps/sim/lib/copilot/resources/persistence.ts index 0407e61dd38..c389adc6491 100644 --- a/apps/sim/lib/copilot/resources/persistence.ts +++ b/apps/sim/lib/copilot/resources/persistence.ts @@ -3,7 +3,7 @@ import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { eq, sql } from 'drizzle-orm' -import type { MothershipResource } from './types' +import { GENERIC_RESOURCE_TITLES, type MothershipResource } from './types' export { extractDeletedResourcesFromToolResult, @@ -42,7 +42,6 @@ export async function persistChatResources( const existing = Array.isArray(chat.resources) ? (chat.resources as ChatResource[]) : [] const map = new Map() - const GENERIC = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log']) for (const r of existing) { map.set(`${r.type}:${r.id}`, r) @@ -51,7 +50,10 @@ export async function persistChatResources( for (const r of toMerge) { const key = `${r.type}:${r.id}` const prev = map.get(key) - if (!prev || (GENERIC.has(prev.title) && !GENERIC.has(r.title))) { + if ( + !prev || + (GENERIC_RESOURCE_TITLES.has(prev.title) && !GENERIC_RESOURCE_TITLES.has(r.title)) + ) { map.set(key, r) } } diff --git a/apps/sim/lib/copilot/resources/types.ts b/apps/sim/lib/copilot/resources/types.ts index ff1703ebbf9..41e0fee863b 100644 --- a/apps/sim/lib/copilot/resources/types.ts +++ b/apps/sim/lib/copilot/resources/types.ts @@ -6,6 +6,7 @@ export const MothershipResourceType = { folder: 'folder', filefolder: 'filefolder', task: 'task', + scheduledtask: 'scheduledtask', log: 'log', integration: 'integration', generic: 'generic', @@ -24,10 +25,22 @@ export function isEphemeralResource(resource: MothershipResource): boolean { return resource.type === 'generic' || resource.id === 'streaming-file' } +/** Placeholder resource titles that a more specific title may overwrite during dedup. */ +export const GENERIC_RESOURCE_TITLES = new Set([ + 'Table', + 'File', + 'Workflow', + 'Knowledge Base', + 'Folder', + 'Scheduled Task', + 'Log', +]) + export const VFS_DIR_TO_RESOURCE: Record = { tables: 'table', files: 'file', workflows: 'workflow', knowledgebases: 'knowledgebase', folders: 'folder', + jobs: 'scheduledtask', } as const diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index 41e31df57cf..48784f1a6bb 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, - CompleteJob, + CompleteScheduledTask, CreateFolder, CreateWorkflow, CreateWorkspaceMcpServer, @@ -30,8 +30,8 @@ import { LoadDeployment, ManageCredential, ManageCustomTool, - ManageJob, ManageMcpTool, + ManageScheduledTask, ManageSkill, MaterializeFile, MoveFolder, @@ -51,7 +51,7 @@ import { SetBlockEnabled, SetGlobalWorkflowVariables, UpdateDeploymentVersion, - UpdateJobHistory, + UpdateScheduledTaskHistory, UpdateWorkspaceMcpServer, } from '@/lib/copilot/generated/tool-catalog-v1' import { createServerToolHandler } from '@/lib/copilot/tools/registry/server-tool-adapter' @@ -177,9 +177,9 @@ function buildHandlerMap(): Record { [PromoteToLive.id]: h(executePromoteToLive), [UpdateDeploymentVersion.id]: h(executeUpdateDeploymentVersion), - [ManageJob.id]: h(executeManageJob), - [CompleteJob.id]: h(executeCompleteJob), - [UpdateJobHistory.id]: h(executeUpdateJobHistory), + [ManageScheduledTask.id]: h(executeManageJob), + [CompleteScheduledTask.id]: h(executeCompleteJob), + [UpdateScheduledTaskHistory.id]: h(executeUpdateJobHistory), [GrepTool.id]: h(executeVfsGrep), [GlobTool.id]: h(executeVfsGlob), diff --git a/apps/sim/lib/copilot/tools/handlers/resources.ts b/apps/sim/lib/copilot/tools/handlers/resources.ts index 885e57e30ce..e7a970f3992 100644 --- a/apps/sim/lib/copilot/tools/handlers/resources.ts +++ b/apps/sim/lib/copilot/tools/handlers/resources.ts @@ -1,3 +1,6 @@ +import { db } from '@sim/db' +import { workflowSchedule } from '@sim/db/schema' +import { and, eq, isNull } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { type MothershipResource, MothershipResourceType } from '@/lib/copilot/resources/types' import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' @@ -83,6 +86,26 @@ async function resolveResource( }) title = `${workflowName} — ${timestamp}` } + if (resourceType === 'scheduledtask') { + if (!item.id) return { error: 'scheduledtask resources require `id`.' } + if (!context.workspaceId) + return { error: 'Opening a scheduled task requires workspace context.' } + const [schedule] = await db + .select({ id: workflowSchedule.id, jobTitle: workflowSchedule.jobTitle }) + .from(workflowSchedule) + .where( + and( + eq(workflowSchedule.id, item.id), + eq(workflowSchedule.sourceWorkspaceId, context.workspaceId), + eq(workflowSchedule.sourceType, 'job'), + isNull(workflowSchedule.archivedAt) + ) + ) + .limit(1) + if (!schedule) return { error: `No scheduled task with id "${item.id}".` } + resourceId = schedule.id + title = schedule.jobTitle || 'Scheduled Task' + } return { type: resourceType, id: resourceId, title } } diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts index bdbbb635244..8adede9671b 100644 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ b/apps/sim/lib/copilot/tools/mcp/definitions.ts @@ -240,7 +240,7 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ lifecycle: { type: 'string', description: - '"persistent" (default, runs indefinitely) or "until_complete" (runs until complete_job is called).', + '"persistent" (default, runs indefinitely) or "until_complete" (runs until complete_scheduled_task is called).', }, successCondition: { type: 'string', @@ -443,9 +443,9 @@ Supports full and partial execution: }, { name: 'sim_job', - agentId: 'job', + agentId: 'scheduled_task', description: - 'Manage scheduled background jobs. Supports creating, listing, updating, pausing, resuming, and deleting jobs that run prompts against Sim on a schedule or at a specific time.', + 'Manage scheduled tasks. Supports creating, listing, updating, pausing, resuming, and deleting scheduled tasks that run prompts against Sim on a schedule or at a specific time.', inputSchema: { type: 'object', properties: { diff --git a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts b/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts index 261a4f69065..bdead9b45bc 100644 --- a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts +++ b/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { jobExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq } from 'drizzle-orm' -import { GetJobLogs } from '@/lib/copilot/generated/tool-catalog-v1' +import { GetScheduledTaskLogs } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import type { TraceSpan } from '@/lib/logs/types' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -124,7 +124,7 @@ function extractOutputAndError( } export const getJobLogsServerTool: BaseServerTool = { - name: GetJobLogs.id, + name: GetScheduledTaskLogs.id, async execute(rawArgs: GetJobLogsArgs, context?: ServerToolContext): Promise { const withMessageId = (message: string) => context?.messageId ? `${message} [messageId:${context.messageId}]` : message diff --git a/apps/sim/stores/panel/types.ts b/apps/sim/stores/panel/types.ts index 05012aee9f8..43192146ccd 100644 --- a/apps/sim/stores/panel/types.ts +++ b/apps/sim/stores/panel/types.ts @@ -31,6 +31,7 @@ export type ChatContext = | { kind: 'file'; fileId: string; label: string } | { kind: 'folder'; folderId: string; label: string } | { kind: 'filefolder'; fileFolderId: string; label: string } + | { kind: 'scheduledtask'; scheduleId: string; label: string } | { kind: 'docs'; label: string } | { kind: 'slash_command'; command: string; label: string } | { kind: 'integration'; blockType: string; label: string } diff --git a/bun.lock b/bun.lock index 3aeb9c70bb8..722893ecaac 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -504,39 +503,39 @@ "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.101", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.81", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MU4KXBasSVTcP9U0mtfcnW9ME8fo9Hsf9ZOaz0SK0qHAYwxck9Dmh4dyBGZqcopYHkhYQPskTzYJq0ARm0hHsg=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.102", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.82", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CeNdbx2gmtwnWdRoHQNbLbly2n43hgytqM4J1OqsBLLEkuSgPDaf+ZrFcQBgxZKv9xQX606yaNN43FGS4VSIpQ=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.81", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0iqx9hZc9xqdhxOdZkYJAKuCs9o+5a86gStYl0M7IBZzmx6jTDrynXiOigDjH3SQrmLclLCspTjW5E6YFrlyHQ=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.82", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bMiG4rUwdgQ6+E2klFSaJ1BHO65zCrfHRqC/hkdhA19tmx8FqLoAmXpx92dcWsADFMtAzQd3Q9kRJ8zk4/PpnA=="], - "@ai-sdk/azure": ["@ai-sdk/azure@2.0.109", "", { "dependencies": { "@ai-sdk/openai": "2.0.106", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0Cd/YzLkF12v8NEyowNdZ3WLGXzdaQeBd4I6EreuQuCnSgO+SwhsS7RbnVB4/FVISgoctSf8+/ojkO+gAC47Sg=="], + "@ai-sdk/azure": ["@ai-sdk/azure@2.0.111", "", { "dependencies": { "@ai-sdk/deepseek": "1.0.43", "@ai-sdk/openai": "2.0.107", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bnTur5rNw4hH/AUjv8QSy60lJ8FmHAS22IwsSSta48dmLirxXdEIB3AEMFYEhKd6TFaFeQA8unN7HOGu2xF32g=="], - "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.44", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.39", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2w7+jq0bWEF6McgWPb2gjaEx1TpqdUq4eyX/gPLTp7HzfDZKEVmmVXRvnKvjzBP/VH7xW4OT5jhTpTPTfYNYYQ=="], + "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.45", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.40", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xP3/RC0oG0e7zLssjLn/aFCzVlqTfePYq/LmMtPDBH2whrcFXP++UkhOd/t20HftO0LNcMORLLp8xkLwkhKUKw=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.41", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7L2Do5wk0xRe0Ox8CVRF9B5b5SPemZP16ZbyBUAlNtO16EMFLSX8LXGeQREZ2SOQ4pC95BwSXThcTkt1JbFNlA=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.43", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RQ2FiCUUHzbGchFaYWvpDpOpDGYypPkgtB1NJKvOXwiqD7UqaCBa6yYjKFHHZMXqeS+lkkU8v6E2pZXigwrh1Q=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.98", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JNMc5Fbz8AwiLIR3Ar/lV2egbLFE+A5nfwbRKrdfgusoVN2VjgMX2U2KCLux5iWD/Q9+rg9+njHPZNw4HmzBJQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.102", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5OxMbtF4AFeyHjKS2smzCYDvjkE+nWktLCS0xsWNnI71cP0mTKXqkygta5lq1WTytVVkonLMP9J0OqEbteBzfQ=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.74", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Lhw1742RXc+4pRIvqVXa0jdl5+qdpmw8lj0lm6OchUg9rVGHzymlaxe7CDiYX5U2af4jbjKeTY22LDi3bIycgQ=="], + "@ai-sdk/google": ["@ai-sdk/google@2.0.75", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Aw2pN+6Ur4dOxG0Bs39gb+Q6KMbPBrYl8e7fZzmh9Sp9vEKyLA0EuaXzjVEX2qlpSNC9gV8YPgkoyWE647uXVA=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.141", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.81", "@ai-sdk/google": "2.0.74", "@ai-sdk/openai-compatible": "1.0.39", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+PjbZu63x+7RABQpAnNcJ0+EEZjKt3nQESQszA4Gyv9rLajob+FvxRJWeiLcKDsGIQdEFBknDrI5KLLSm7Doeg=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.142", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.82", "@ai-sdk/google": "2.0.75", "@ai-sdk/openai-compatible": "1.0.40", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bQuZKTMRZ2OrVgk7Tq2hBdOjMuNwktcDx00asmRF9nGThluAqOF+KZE04H6+24xcWgf1LWddV5okL2CAWsJKNw=="], - "@ai-sdk/groq": ["@ai-sdk/groq@2.0.40", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1EL8D1tyjOKjCFUt8XspDoA6zxDcalMsLR2O56ji8QklWsAPaf4TuMJAvf5x5KDrkuJaSAjk94KvPH5hOX+VNQ=="], + "@ai-sdk/groq": ["@ai-sdk/groq@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rDz7BSEuZaozWm6PAz1GuJgEltQdX1GBFk3gmdTF5a9THTUZLB1YOxMt1BUWrYjwxyIJkO8mUtpC2s/ksjmSkw=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oBR9nJQ8TRFU0JIIXF+0cFTo8VVEreA1V8AMD3c77BJj/1NUSBLrhyqAbX9k7YAtztvZHUdFcm3+vK8KIx0sUQ=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hoaNkUPgNQdTn8o0lgGERiov6b4WvgKcUXqACJt/YJ2Eoh5bvVsmfP+EofgWMnizAO/FLKhvLVS7+1jEnMPejw=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.106", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EFC0rpo1wfe4HIz5KZCE72edP2J7fOeR7wPXzjCDljaTRB1wectKDIKRLowpU4F0mbcJ+XScAsoYNPK/Z20aVQ=="], + "@ai-sdk/openai": ["@ai-sdk/openai@2.0.107", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cWU7w1c0FhDiZBFfi0rZO+5qYlD+rCDE8Il1X0P6E7wejtghgyW2I6ihPNTXfrLT2J8DVeXnrwQMFfxfVNzstA=="], - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.39", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-001hdQPPXxYBWrz5d+eAmBVYmwzsB+guIey1DFXi1ZEE5H3j7fRrhPpX55MdM9Fle2DS7WZ8b3qkumCIWE92YQ=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.40", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K8SzN+Hpr25Fx23VurW9rtg8dmUgqX+rDI7LKJFCIJcM3TIu3PfknxYlRGivDIrmsmgzH3PjvMGuCq+E8ZW7rA=="], - "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ymXWoItR4tRCIQlJcpn0zk4jBUU+j4SDnliz/z1f5U6rWxNY1ttxFCk4uZ+6Zt9e3VjQTpA9FK6cOJt18JRrKQ=="], + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.31", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1oyQEfVG2GtNAUqvn9XvlngGwHN1U8fCmHOullKmVdE37yxh1a3Af3CZNEMf/0lYqaVmQBOitnPMmb+d75G7Xw=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dNciNI4knep6z3cqDNng7yORCcBnEDBHZYj8rJLcLn9pLzEtNVkf6WLg9HR6AnVDDRxHsUeGAEqyF8M+FAtRgg=="], - "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.39", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V9reHPfWeaIt6fu03lVbjZDuxfdplS5jdmzVchVBeUug9VqIK+9KQELcPvdWKdxf+ov+sCoShN/O6dYfPPD5Ng=="], + "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.43", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.40", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FwcKwNKSjkCc80yD7JFtHOXxroTpFD/FF84jfiGlRGTfv2rQpv3Pto9h/NcKjmDXXr/H01Rzo/TodrsLXq0B8g=="], - "@ai-sdk/xai": ["@ai-sdk/xai@2.0.73", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.39", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-U+/rdtqgDcloNSX7TIdRjYQooVydYdauQvLSP74oQcnE5N0/DD81yi+RvQXYYq47dDIn2H4exgr7XkBm4x1yDw=="], + "@ai-sdk/xai": ["@ai-sdk/xai@2.0.74", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.40", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ff24f/G13EjKKw411XQh08/owBorFvaLf3LEuDvDnDz4XePpkYN6oXdyHpZCDrvUD0/+7hbwaGwTXfm5nmqPGQ=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -568,7 +567,7 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Pt1JEVLu02jTWzpcUzUHciiWScyZg3JpHCTB1h9DtDPWY3dBufBnFJAevVHali/bAkmMdMhYUD8tH/VvPuBkUg=="], + "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-RMCrCteiUwYTEv2G9zfP/BEuKHv57665vVieJyp9cf8VgilWxP/KrWVtMdfdDlIH8nFhvu3rIMc29z3ebGEZ1w=="], "@aws-sdk/client-appconfig": ["@aws-sdk/client-appconfig@3.1032.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-node": "^3.972.32", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.16", "tslib": "^2.6.2" } }, "sha512-WcS820syhSamz1PcZUvdUXf7FUm3cpze+hfMvDKzPojrh/zFO5eVopzhBGEkDFXiHFD0qel1ZgE5s5AkmH9fyg=="], @@ -608,85 +607,85 @@ "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1032.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-node": "^3.972.32", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/signature-v4-multi-region": "^3.996.18", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FCLc5VWb+yz1xb/Jv0sXFGqIIs+bHZQWBKbPQKCuypF3wU/7UFygXuSXo9uJfwISKNGVHJwp+0136f8mqmzRcA=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.20", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@aws-sdk/xml-builder": "^3.972.29", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g=="], + "@aws-sdk/core": ["@aws-sdk/core@3.974.21", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@aws-sdk/xml-builder": "^3.972.30", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-P5JAHvn4dTi96UsAGS67LVOqqpUNNRhnfFXqzCYtdBIGZtqBue4CXvRr9YenOO7PALj/Pn8uuyw53FBCiCYw8w=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.47", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-3YoPwJczcc+MtX2xxXaYaOOWO6xKUJr1ZIIDIFuninr51BYONVVcF/CP8K2xfVRC/PztJjqKWxNGFH7BWQAw1Q=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.48", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-2UtGUPy+x3lqyceHrtC1uEuVxBZbDalPF6KAFqBwYgm4edWdBrZKNnCqzDs7KynWUvEC6mrR+ojRk+ZgQz9C2w=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-login": "^3.972.51", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.51", "@aws-sdk/credential-provider-web-identity": "^3.972.51", "@aws-sdk/nested-clients": "^3.997.19", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-szg1nnebqC+Svv6Vfsdf6P/QK8x5g/ghG2CKa/1WkHifRnq0BBmDELj2Qnqk9nPsUvEu/OEcYic97CPLpKqF9g=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.54", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/credential-provider-env": "^3.972.47", "@aws-sdk/credential-provider-http": "^3.972.49", "@aws-sdk/credential-provider-login": "^3.972.53", "@aws-sdk/credential-provider-process": "^3.972.47", "@aws-sdk/credential-provider-sso": "^3.972.53", "@aws-sdk/credential-provider-web-identity": "^3.972.53", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Hx4gO4YRjFwitf3MVl3cDwYe1aryJthC4txVl9b+JAURovA50M2ywf9r8j1E/Q6SCTPT4qQpjOAbKYIC9CG+Vw=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.19", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-csHFsH+/VjnI40oqm1l1OqMY4B4kza36DbfcbHcgcbobgjebasqUbTU34xvwUkvtoNGGizbfyMSlMzJWUPv3dQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-+71sluhkgPqdhbbD3UDwUpj24GCkng9HQx6z7qoBFb8dwkF4ktpOcVKDeHpgg8PvBgLYwAnUYLTEGRC/PniCiQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.54", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-ini": "^3.972.52", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.51", "@aws-sdk/credential-provider-web-identity": "^3.972.51", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-vinTSQtziNHxi2nqXF+76jr2sO44q88Ind1qFFVaotNgBaC1rcWDjBug8yoE8n0ov33s21xks9WY5XDHH9SENw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.56", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.47", "@aws-sdk/credential-provider-http": "^3.972.49", "@aws-sdk/credential-provider-ini": "^3.972.54", "@aws-sdk/credential-provider-process": "^3.972.47", "@aws-sdk/credential-provider-sso": "^3.972.53", "@aws-sdk/credential-provider-web-identity": "^3.972.53", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-iI+4o0dvQQ4NHel4FMDiFy5q2gaU/ryLK3niOsoPccAt9WLFRkV4XTYPWRr9XvmBUqEzXG73S4p/8gm0Lu/W3A=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.47", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tAizPm9IFo/PHn06c+LQJlzfY2AGOlyF0CUljFejrU6LcZBjnk8pmbZK3/xoIDdnIzjEdbClfvY3mXfr818ZEg=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.19", "@aws-sdk/token-providers": "3.1065.0", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-60qhpQcSDIKIr0AuBlmJezKX0b5nbJPCINiR49N9yJXrEI5tTRwsXVBr0IdSvvsNJyqgiINyoBd++Ed0yvggbw=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/token-providers": "3.1069.0", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pUXE3fu4tfEDV8BksIgf4dXvuIH10FhwHMl/wu8rBD5T1sMpryQWFVitH3kdPS90wlgrGYJQ/meQTSPacyZfeg=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.19", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-0X5eWsUIp8ItRJeJBBrhQAPzc9AQelDetRTVTsycCAISCCzM17R4hs/vFAPeQ0o0B35sciLiqe/Pwmml909cZA=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-JmMGlhVvSj8uSG9CpeDkJAXT35H89tc6v84iMgEIE75q4yp1MKVVKvopv6Gg28HJIR7hMNkojRF8H2m5W44wyg=="], - "@aws-sdk/dynamodb-codec": ["@aws-sdk/dynamodb-codec@3.973.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SFfxiqVgWeIe+RsJJNAMD//2IfehT4bLpGyNJRB0MgHmOIJtdcfMnR1k7KYyaHokSoQVdncVa9O9DIGa4eqcwg=="], + "@aws-sdk/dynamodb-codec": ["@aws-sdk/dynamodb-codec@3.973.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wfAWZ6oIrsDOFyYm9bDQNva/WCmvIrVqP3dSCePN5YYWCGWWXkikn5YC0wPSxF92M8kQFPfdVpMaTTV1mRk4Lw=="], - "@aws-sdk/endpoint-cache": ["@aws-sdk/endpoint-cache@3.972.7", "", { "dependencies": { "mnemonist": "0.38.3", "tslib": "^2.6.2" } }, "sha512-LkwS3ZOUNL5kHzmz3dDx8lE3HOhZmf2VGjbJ/tMUZJYWWl3J0RJTZM7RFz1MLt06WDVvlShcAjY/RzhYlqLL7g=="], + "@aws-sdk/endpoint-cache": ["@aws-sdk/endpoint-cache@3.972.8", "", { "dependencies": { "mnemonist": "0.38.3", "tslib": "^2.6.2" } }, "sha512-bBmkG0Dnhfq0/T4Z0PpUr7HkncBVaWvvCbvafeaUM+yC9wa8GGjLJmonq0QL17REB9WivgGeYgWQ5A80Uw5UnQ=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.21", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-mVC0hOmwGJmNFezZ+wM8Sqfap/LjsMavEf2Evl0YWrLAcrdZOEdjnY8nRvgakVViWJSGm2eJxLuPVHGdeV06kA=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.22", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ=="], "@aws-sdk/lib-dynamodb": ["@aws-sdk/lib-dynamodb@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/util-dynamodb": "^3.996.2", "@smithy/core": "^3.23.15", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.1032.0" } }, "sha512-rYGhqP1H0Fy4r1yvWTmEAx0qqy1Zd9OzI8pPkXo6KSEDjZ4EwU+6QN1V+KLX3XTU6FQouF5LTvqLtl/CW4gxyQ=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.23", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.50", "tslib": "^2.6.2" } }, "sha512-X0kemMevWxLa/ai/iBV6Lh6V+DqdKbuY5zX4nJ0HlEL3jgPdRSnxTNrGO33Er+2N+fLLriDyriw1O3DFFRR+zw=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.25", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.52", "tslib": "^2.6.2" } }, "sha512-zcRjdhS46gQ+omEKod2Q83A+42dQlFgQP9GfsK2XcDCli8kzA3q1QH+hDpIZUDbKaXmkTSn0JG3WP5yds5j38g=="], - "@aws-sdk/middleware-endpoint-discovery": ["@aws-sdk/middleware-endpoint-discovery@3.972.18", "", { "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.7", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-1vKJt/6MBB/MBMRM3qzCMdW70syJY8u2DH+dq7yCnPn7wVJmyeAzAa/sK1lIbbYh8BVLbM5FspsT4zbe885gOw=="], + "@aws-sdk/middleware-endpoint-discovery": ["@aws-sdk/middleware-endpoint-discovery@3.972.19", "", { "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.8", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FMgyzUq3Jh+ONRYxryBRNdBd+FUX8PwRl07ccQknNdoms6KCeAEusCkl6whqpDrPQ6OH0ddeSifKyqYSs2DLIw=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.17", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tdbnXbw73ww62ABWP0G0Z/euvFowEEvAoi/zG4NaZo7HJFpfGho/Z65HyVzkJLT1cMsUregr4pTyxljlarT0wA=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.19", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.50", "tslib": "^2.6.2" } }, "sha512-jg1aPMLMCBcaF34ZyqvP9Fbv2s9xlbkEMiQZWsT5F3k9bulA/wrCejLMgAQxHSCruvuK5IEmi4MErST/Q2ZAzQ=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.21", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.52", "tslib": "^2.6.2" } }, "sha512-LmmKxy26I++tBdJVTGnghGFff2xrv+vryfKfbxoRb1la50DY77N1ePclYdDat8/tO6nRXA2EFXUBjY72jfHEcw=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.29", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.4", "tslib": "^2.6.2" } }, "sha512-ALTHDXk6YWVDfAWIHzXyaTZ82QFoMWhHENXlO61lv4ZqSMl3cvh2s0ZVOS89qbtw9LRJhIDoZaaC9FYo/Z4KLQ=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.31", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.6", "tslib": "^2.6.2" } }, "sha512-Yzj6NRYVZdBaCp7o1BwHGyeDBfixdeToLIAMprshIITEdl9wKVSiidVOfeaiH8FyeC1hBmBfDZFvs/aH1Y3xpw=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-+SRZL54/T9Ryxa/NmHM7WZAliU6wnfzeosT/+4IVuTgq0zSCpPx2j3yaEP4JFZlvWvoOCbKgr+4tBqSAG/dl5Q=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-nLTYWmLcXy1qwiDZdXMs7PHrQ8sFc75vDplmC73u91WzpXCDGplcMnhTYltKijybXtUFkGCj4WRwZsmjBjQh2Q=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.16", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.50", "tslib": "^2.6.2" } }, "sha512-c7qT6vMXwzdDbXjexG8jknN7itfa4N1thiZMEmZzTn/t/ev/j0J2HF/60ympIO/iYq69qHOprU6WZXBcppDDJQ=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.18", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.52", "tslib": "^2.6.2" } }, "sha512-pnAGLICTuUV+79LC5rEtCapqSBzNU8LUMgNP5mdfFsfQXxDcwyTEP1mcVwz3id18QOcB6jSqHbqd0MJZodyN+A=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-8oOvo7XNDeOlwa5ys2Q0UTLCKmtvRiqf8rOU63lKniPmP3dnI5lnoy9dteZ79lxb9hmXCrO28aZMqds6C5AEoA=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-8VkkGI7+uxaX5LLeTGE1okITrM9wZinFDDDuLm2J1kBiOvID1bx5p84tpa5k4v0o1asq+5nZFsdKLdyfc9o6hA=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-q4H/CoOYrTbyAW0d9RrHf9kTYKVXpAwoK0VEy3UT2Asad+6aa6vzQgz35dh20tRA7zlEo/Nsyjy9PVlHgdq0Vg=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-ZaGQf0chuk6akH6+yfbM/1TCYU+ktaCcE9ZBHTmk009lKknQRrnjZDSXJhBCe4QbylcBhTbIV+x2tVluSgm6dg=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/signature-v4-multi-region": "^3.996.33", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-yOXn9mmJQQODpbmwQB224IX1PLLneyqInX2Fv2nEmSHWpJj54nrzdrUT1TGQk/s8mr+XPssDQy1at/8GS4EFVQ=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-rerjP08onRqkBh0AcCqip6GkKvESapmLoTgi1xysZ4C6a1xMrIMtTBcEbUb6EY71oeajnigeUD4KwZjtIO+aWQ=="], - "@aws-sdk/middleware-sdk-sqs": ["@aws-sdk/middleware-sdk-sqs@3.972.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-PVAj7VgWK/ZxCXnkgC4B7cdJyUN99Nsr7IEduHt4A1GieuB+ZnU5bSifHwapbr17wrFkmdxfSh+aA0Lj+Ads6w=="], + "@aws-sdk/middleware-sdk-sqs": ["@aws-sdk/middleware-sdk-sqs@3.972.31", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-56ifsBmK9bLn5EE/t6c0nmjOB1BO8cJDLkA1VOlsN1GR85ROqnaCwVDspqcwsLaBDgPlwyYNedoDIoT3t6Ho1A=="], - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.16", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.50", "tslib": "^2.6.2" } }, "sha512-o0oPX8JkZd2Fep0TFAE0VY4dzi86Q5alxSwd2O3wR2M9zg8/zJ4dEpkw9kGCr3mRghP3E5nWLgsfzJ9RKFwVnQ=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.18", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.52", "tslib": "^2.6.2" } }, "sha512-Emeqtr7HJbVmkcMeiz86nxVbekdEYRMKkKuYKqSSE3M5GMOkG1eKacgQn3LS/iZ3m0mzApGN/6LRnyomgHjpKA=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-5JIDcDFWy3DUW95sOlXLbeb7RkGFUcNh1QidKsznqtlm5YGsXP0EGWaqzxBTvVmOhqKs2RmNmI6w9V/5dS3CLQ=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-J7+fiPR7axyvUSvckXnAiutX0/6O+0MvXS7BphQAkm5gnMqQPhw5Np15AnPZdjp/DW9WJeTczjiR4W484Rlz9Q=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SCW06Zjugn86pq7+dxGnFcyWJuEWHT753HTU/Vj/OzVxP+NoShwdAr4ynxAcvWL883OgRVbSqW3ohnjIxwXjjw=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Agv95NCgYyvuYUXt2PcFcOMrKCkhBFPhoH+nVMQh85RcXSCQrhAa4475plBOeomCihP26vKHT5KinVQT3iD14w=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.20", "@aws-sdk/signature-v4-multi-region": "^3.996.33", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-P2Otgf15GBJMKzG6j5Ddf7w+Kz6z2jvesMy874TD3jlMfDWNK7clJeUd7hgigdeVOotjoUP4emcTWVdS9sfZDw=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.21", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.21", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-eC7Vl7Qom/BGhZjG9GEqPwdQ/fk45hg1t5LP4EUxG5d1fdshLbaxCiwh/tszUzDX/4mW40mu2QsbeJJRPBbqUw=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-WY6uVMsq0EvxY4BcYhZmG2Ivd1EzNvZAqsXFlL3pTPMG0P4J83TYVQIs8P0nd5lc+Bp3llrYwggruvXzrfUtsQ=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-aFc/pn5pfnhZCEhyjv/D9kR2c9WSZdkX+FPrsb/AGvY7TiAkHqJFeIw2xqbgeiAhy7W3/w41Mi8Vr52A74EDug=="], "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1032.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "^3.996.18", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-LFaI5JQhiOmJDjKK02ir9oERU9AmxdyEvzv332oPDzAzWeNH06sZ1WsF3xRBBE5tbEH2jIc79N8EqDCY0s5kKQ=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.33", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Hn0RThJEbyOZWV2PV9Z4YD3nitGPxybmyU17dSe9b61WOBcKnqS0WTtM3c1zyZq9WnGiyrfi/i+UBPUk7cM8Ug=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.35", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg=="], "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-n+PU8Z+gll7p3wDrH+Wo6fkt8sPrVnq30YYM6Ryga95oJlEneNMEbDHj0iqjMX3V7gaGdJo/hJWyPo4lscP+mA=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="], - "@aws-sdk/util-dynamodb": ["@aws-sdk/util-dynamodb@3.996.4", "", { "dependencies": { "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.1064.0" } }, "sha512-Gel6Qiof4NWdtGT548FxoAkmmKmvsEVQYzbtC4RJnwgh9z33gmiYSJOGnKZHvZJWC2SgjS6AKe6DfuGCqU4vdg=="], + "@aws-sdk/util-dynamodb": ["@aws-sdk/util-dynamodb@3.996.5", "", { "dependencies": { "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.1069.0" } }, "sha512-m9bdmYq3WtbMHAKGALw9XWiMBfKu5T8ukgdJT7Mc/d2oOwDGNFmhsnnkQ18xomoXo/ZHxAuIDi3Y6slsblW1Mg=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-W79LutZYmCV1WGe0UVGALMna3xLGP3wv2zLzrBEgc73CtzxKBxb5IpMedbS6Ej80LCknSgqFjsTtECG0IInckw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-mwbTLCYFICfvigDoQggOe8utOXSlYLJVWD5KAs7vmx2gZ6b3HpQotAcrti3hrxlBX8mjr6101bIXd4Cz6fbqAQ=="], - "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-ygzBtG3xDxvMWTg3EjlZ5FSYxUiRCsCZvKPk+sOhXeMcDTdzPqIGQipiUiIYJm/Om8h8qXyhchMb0baW1PhE+w=="], + "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-+KAkQBGX3CSFd7/xs+pz+j+D7rWXj4fviHn+Ykb4T4cfrOJFI3yfy+y+wvZ0vUIhVNy2wEKwFkhk4+tAoMFppw=="], - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.7", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-Y9MPH4VZaIebwxiVK6dMzSt5L04oyNiK3NNwFe4qP5B2Hfo+pmEVpSJSa+gARPtcJeRyehUMPu5/I9DLdW0cBg=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-Fa040urz+8bwxgnG5KoSglP53d4l3jtby65qO564mjQ28o5PO4FmkNWy2atSln2pUjKmCmpbSsV2pLgcGULRIA=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-wsmSmHTrK9lV+5SKBekp1J7W6PufdZE08X0xv5Lz1OdgYRmyuYDy/++SslKdXhcVQjxLtzMTZaLqqLZmTx6OaQ=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.37", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "tslib": "^2.6.2" } }, "sha512-/F4Y0+iREEUvVCPQkJqzRnly8MAihbQy/s9847yEl9TLsXYEKMnrMIptsjV4owcNgm2l6zqKEZ7SDXv6JPjrRg=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.29", "", { "dependencies": { "@smithy/types": "^4.14.3", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.30", "", { "dependencies": { "@smithy/types": "^4.14.3", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], @@ -842,7 +841,7 @@ "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -1052,7 +1051,7 @@ "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], @@ -1076,7 +1075,7 @@ "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - "@nodable/entities": ["@nodable/entities@2.1.1", "", {}, "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg=="], + "@nodable/entities": ["@nodable/entities@2.2.0", "", {}, "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1154,17 +1153,17 @@ "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A=="], - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="], "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "@opentelemetry/configuration": "0.217.0", "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.217.0", "@opentelemetry/exporter-logs-otlp-http": "0.217.0", "@opentelemetry/exporter-logs-otlp-proto": "0.217.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.217.0", "@opentelemetry/exporter-metrics-otlp-http": "0.217.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.217.0", "@opentelemetry/exporter-prometheus": "0.217.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.217.0", "@opentelemetry/exporter-trace-otlp-http": "0.217.0", "@opentelemetry/exporter-trace-otlp-proto": "0.217.0", "@opentelemetry/exporter-zipkin": "2.7.1", "@opentelemetry/instrumentation": "0.217.0", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/propagator-b3": "2.7.1", "@opentelemetry/propagator-jaeger": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.217.0", "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1", "@opentelemetry/sdk-trace-node": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="], - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="], + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], @@ -1212,75 +1211,75 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collapsible": "1.1.13", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collapsible": "1.1.14", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dialog": "1.1.16", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dialog": "1.1.17", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA=="], - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg=="], - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.17", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.18", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw=="], - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ=="], - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg=="], + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.0", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.12", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.5", "", { "dependencies": { "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], - "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.9", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw=="], + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ=="], + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.11", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.12", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA=="], - "@radix-ui/react-select": ["@radix-ui/react-select@2.3.0", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.5", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g=="], - "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.0", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw=="], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -1402,7 +1401,7 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw=="], "@s2-dev/streamstore": ["@s2-dev/streamstore@0.22.5", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1", "debug": "^4.4.3" } }, "sha512-GqdOKIbIoIxT+40fnKzHbrsHB6gBqKdECmFe7D3Ojk4FoN1Hu0LhFzZv6ZmVMjoHHU+55debS1xSWjZwQmbIyQ=="], @@ -1452,87 +1451,87 @@ "@sim/workflow-types": ["@sim/workflow-types@workspace:packages/workflow-types"], - "@smithy/config-resolver": ["@smithy/config-resolver@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-AXbvUX9aNY2qCLOMCikpl1Df5w2CNFEqbEb6XafG81FJbAbB8avIT7BOx1KDqiO86J/38qKQ3YuakfAfY3iBkQ=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.6.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-NJF/Xc69G68BzZMKMEpWkCY9HjZJzTWztTW4VxBC2SodX+H60xw+NGckNhkgg4uMRHrpDkhWeBeigM3YJmv1FQ=="], - "@smithy/core": ["@smithy/core@3.24.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug=="], + "@smithy/core": ["@smithy/core@3.25.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-TTD6el7tvKyafkXBf7XO3jLOE+qVxOTrLjp/fEGiV3BMfUHK/LfdYlQO9YgZvzxC7kqA3H/IhJXNqQgnbgjb7A=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.8", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-pPQmNdEvMJttv9z2kdYxoui83p/nr32zjMf0aMfmzmGmFEgKXUfy0vXiNg0fx4R5XLQzmJBLM9Wg0guEq2/q8A=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-Ussyv240JxwQP8AmkYdm26wGP/1I8QmIv0ZosgDJDlSzD73FEdj1BOpXMc06VrxX5KxTKhadFNomT2SWutUnpg=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-fON1WZ+FGrHt/nLH290DoH47tYLK1kiAn+reMnc5P19IbxLZTZbrJi05Kv1Tjekinxrs4f5c0SA2eicLzy+rog=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-BQao/dBhLCJqo953N1hadkcF3M/9G+i6qIgnMupfdpBQomwyhfV7Xfc5jjpCkm8HxfzaWAGrM/2nNnzronFqVQ=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-VIViKuJIgQ5eb66Su0LshXQNf3oJ0QXQ3gDg/rXJ47mFN6wmoolUT7OwczRdjpHGIH4T99aPSLURb9YYoLZqmQ=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-OUoNRXJGZMM4ivoU7QIzOvCLbavD1YnadNEairrtYhTi+gmGhyn3c2wToL9CxEs4Cw2Ab/KeQM39T1K+/e9YdQ=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-JtUvQ4EP4+KtNkwSLFRIzHdFftfZ+CQ8g95xT6uBzCFCu23Lt4sr6neQXmNHLM9RJ9Vw20LdcTBXtw3h4J1qeA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M6FeKRMi3oecpTy4EL5n1hLPWydw+xInFYQIzjbGYGBnFtW7IlJjnXrKr/Ev1GpMtmw44QCmrl8+ACEFPmRsIg=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-N8TeITQmnPZNKgjVaoHm4pOUPAeIPWAqTZVhEltxEbWsYciC6NezCw/TjUudgoiU8ljpvpzxQiZ3TLWMvadNMg=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-OG8kBYAgX7lf32+xLzgirvuLffn1KNoszaSiButt45i2cRa5irk8LQXLYQ5Smij1SBTN4KMNcBsRwRrLPfIGyA=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-/8D8rOFs2VEwvHwsx68sb6nE7XfVr2wbJTbC1YuKBHPhHeMnOt7IHxr7CoT5wBWujdV4fjVoLPn1BXXP4Ijlow=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-3R52ZjnJhey3zlp9K1ixVGQIO9NOaHF/MJR5wLbFsEOn4EG8M41Cp6a7duMLEw7FYEO+ikO/5Q0vghErT3uaKg=="], - "@smithy/hash-node": ["@smithy/hash-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-lIZyQ7gDxURrnfkjalM0lKmDnfZYuPzNBYlkza3czPTQNVYsg4e0o90Zx/RpxhamKKOGsQGCsopp0ULsJqltNQ=="], + "@smithy/hash-node": ["@smithy/hash-node@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-MkyiJfdnDlBdmq26Cxskw2dtX6V/EgTjCriPc7Gq0084hncjIFVJ26IwHpauXJT2w79B4umF0erKi4epBR/WDA=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-Ziap41FoxpKqmlO9IE68NeFwPKhUJD4PVNcCQ2tl6IUCPSj0KykIuAPnJNWIQbWXvApwCauhRNlAFdt9KRvDpw=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-wdhUbsxG+xpUhtpPr6Qb5si1JizQjLJwZKP5uQQrHLIxEBJ27wrnWt6FEKs/6BBUA0aqfTbiZ/aUr6IqRfl8SA=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-jUH1Eth7Sgn4KPBX5OKYDRpNjzul7AzsIhxKXT1rHXPTSfY00/7Kb9RtNil5SDAlPPsxaUiesR/rql2wjackmw=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-KWyzbLxpEcr4iU8A/Bu4zZN9w9LdXT6SO2jfbwP21xdNr2JyW8XBowOKViG/dHp912ekAmtJ7SDfPapj7yS7JQ=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@smithy/md5-js": ["@smithy/md5-js@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-LYcuBrO9oiajdRFHyFx3FJAWNKrP89s0grI6mcfpwTAeX2ZJ/9Xyi7Imghh9LT6CIcAy6/k6/MpoUiPNjXr1/w=="], + "@smithy/md5-js": ["@smithy/md5-js@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-/TkyVulxTrirvSUs8hvyYH1V/sKxaO2RTmAW7oh6D6wyNPh8TPUxpk1RYSY5wd8bMZpKfh8FSEMIkLSTnN/Pjw=="], - "@smithy/middleware-compression": ["@smithy/middleware-compression@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-wZQpnjrGSO2IFxhwWNaeRzHh2swSwRGWaCVgQN9zqYdtP98tcNYyqI7YvPeVTwf9CvQTas7xlmR3NY5L1i32mg=="], + "@smithy/middleware-compression": ["@smithy/middleware-compression@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-Kt/HTMOuG3YwaWc06e+PiFziIlDdK8fO2KcYZXUTqzLF4na5XewzNwvmgaOao8TcT63paPdqVSsVdBy7FTg2dA=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-nfpYCrzSFAgfIXmIHFTjOGNeTV3DVF5E5rfi3ZuNfsOjKSpePBOJF3rjyXlWYND0anvxVoqioIwClWCNdKt4Og=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-hLdaOvB2JIZhOa6REhHJHXQavMQC5EvewIiWM/mk9AWGlwoo6QyAXlYsp621AexTqY44558s3e3vzLHwyPhlsA=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-zdG5bJZOiM2PRgL2lwcgui6uwZ+s5y6Qsk/rk05Q69sZJT6oi1x+v8Kn++V/q9VY94EgOtEe5kivpu+eGau0wQ=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.6.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-yPaTGexBoXq70QMw/dIq/E4pLQMgBtSmAV23XyZm9UcMoGMS7efa2HMy+LvhlnDgyqCeXn8mQ7k+e4uD6rbjew=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-MWppaYUlc+W4cU2JZnYuMFeOxCWbKO4A57BWti6aCb7hRBK3+CL6llADGpX084hjImsqr3EvCGewArOj7G81eA=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.7.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-Br+n69+Hc6HwZZmRfhrEB7q7C6MZBghxlCugZHnvnPJN/bsMYG3d4hzhXjJr4EyBkxhe5hcvtZpgUDJhdmV22g=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-I3fPVYKKEog3a3qdqt1nttP1NBuQOAlNoQxEp6j5pMogSx0HHfid63difhcDgslV6p1XsTXG6D6ieTe13ycJtQ=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-bDnLiVuVciCC4d2n/PCcGJrKwgQupNIeuMNZvkStsGGeeVJ9WDjTpDwEYZTiXSIFszvzt7FVX3l5rsB3puNDbA=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-QhNiWfg47Kl4SJHmuQvnlzCtlD1eX1J7d/vuuttIE17Ra2YUKp9Srv5lCwa3OvoYaSNWMKYn0PjGIsfCLMJsEA=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-tZUD0fE+/aLzLS4b75SDyQXBybPCI9UqwEAhDRmME8ObjEtnMnA6Hrt0FCNMN+JPoCtcrbUS0cHPXFTQMDtgoA=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M+gG6eQ0y073mSmNB+erRXJvwpsqsN72ol2w6vcd8FEKeG7pqYK0JvzfVqONkPj2ElBB2pg+cU13I850b//Wag=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-hwea2f5OKcsZMKGgMYzWyclQKoMMbXzFVuv4033sc23dEjGOscqQ0hGHLDQcSneSsIZ8WcwxCV9y+ou34xoizA=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-Mq7TNt/VhlEWiYRLQGpzUWeUxh899UGpjKh7Ru0WVIDIjnE+cTRAn0NYlFQ6bWfsQnKnpCbWJj86HzmcG0qEdg=="], - "@smithy/property-provider": ["@smithy/property-provider@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-0rhHv1Ww27kajF6qewme2aRtJmKFtSwE6EZ2dj5KxdX/R3ANsUugqTnH0tvpZwGiQ3MOMhetuCGFAeKVv3/Onw=="], + "@smithy/property-provider": ["@smithy/property-provider@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-btjt5ZSO1JOrFJM8Wzzuqlzu3pc+iGb3InhyRbHX/LzK4gn4/SnfjCEdNAf912tcgcjfi5b2kiaNGz9vlT1Eog=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-H6S7NyaaL+7qO8kIL7VQ7KyrGnKXdllGzJqvtp3hvDen25UOydKV51qGDVK0UciW125jV3CoLJQy/ihc0OEC6A=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-gqvRWWZIcqmj7iS68p+hrxiOg1fGQcfzNPUlSGJ69hzLHyCyIRApasCpAp/xMGRgb6QqVH/YQhztOYgs+ZI3kA=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-In8gYD2R66EKlGAq9QrNKVrMOGaGBD7LUNp2kUjeQ4V9zNktFIXBPmrCySr4YYo+jVeVL6CnWj26sOamcF0qIg=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.6.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-O/cWx1tDsuyi5I1EkfsrJMnVNfcSvpKmAqp/dKtVfFSIm9Wa/IgVYV7x98OAK7T38eLfRU5/xpVgolC84Ul2UQ=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-vW6UdK7e7gV2wU/tXRsPq4pMQMusb8VymdVOyIFNA1FtyRmEClRFkYDtYI8UcO/HM0wK3qqjvvQs3HOlbgMbdg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.13.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tAf35/JW/DvMlACcazcoIOKOV0JBqyOvxjPTEME9W+m9wLcE0G1rwADc7Ntu38rY5C9OH8jZjpo4tbtjmIjEBQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.14.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-pBJs2oWyl/drgw1lQOdwjXEwEeL36PN/CeRt33lwBu1OZTmoKqQjp93vcjM9fjv5ETsgEzB7WLSX6rYKKP0Eqw=="], - "@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="], + "@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="], - "@smithy/url-parser": ["@smithy/url-parser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-9MRJzwUrlswwHogOR7raDcykuzojZn74qGdQdbEQLVaixlvJuMiIT0g/CejKcmAIgrUVs8brBrnGtmYmBc0iuA=="], + "@smithy/url-parser": ["@smithy/url-parser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-E73GGqNThq6SLLOgQKU5re/iDc1oPk21zPr0t4KUD/sj6qlB06vQX/5xu3H8lTnCqWh9oLr1tXsv2Cpu74TTLg=="], - "@smithy/util-base64": ["@smithy/util-base64@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-V6ApAGvCQnb7Wy1Sy60AQc+7UOEaNQxvAXBLdMi5Zzm66cmX0srvfAxDmg7BGuJ+9H9ez0PPWS/AeFgWxwGavA=="], + "@smithy/util-base64": ["@smithy/util-base64@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-SF3V9ZZ9KotchuyxHdOvi1Y8OO7ZS+mDzoasCIrni9HEDf/BsBqCA9BAKHG+waerz4nutHPGDMRQw8B6VtVCsg=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-+3vGcNHuvzuFLVWL9/wJgucOuQWufhuGhb3oxVDj9SWFGtwkOmtC2nFUwVC2IJoPe45uhs6TAb8bgE4IXDSPzA=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-JU3CDQScfAA9inuNyIQVNbHJ54fhtwXQqwBkR0xQN9lyGkFgFKnzHFgNQonfu67O5kdcnv1bOxhqsfrwmg2i1A=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-T15zTQJ/xKYdS0/3CFckhz1QBbhxmhk/xjL6FKvHKgkJPN4E985If2FI9CcV2kh2v0sfiWMfXVEOKFbqgw4m4w=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-Hu7UCgEGGxjT8pUsaYq4K7tfhShBXYnRU68GRia3H7dzjtU4AX9/jdVS4qhNn4lSdxA+d76iRESNu0jduT1Pjg=="], "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-dRCZKu05AL7KQWrVuRJPotfjCRnvGkCjV56XNP067CRfyTtvgi/Ygu44qrBKb814Hsa52bWwDJ+Vt3pd04BjPA=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-T4/V3fCSnhNg5xLlxxo5H8YsBblVtCnvrSb+XLhUjngUzu8W53uAxdUOKXQTN3HWVBlBOa5sD+BJb6FOqNtkYg=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-tTR8tayMoa0WeRhtMH7j3WpHUtggBXjh7rBdf7j6POYI69R85gpWBW6B32kaJRnlQU8+0gOGAzJj50S7SU1Egw=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-4ZjhBmU8Dt1OFBY8GfKHalfPy0BF4/IrSGMuhiPRc81bbRbLP/rPH65LrLgokm3rd/wzRpTwSEKNeKSAnYHSdg=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-kaB41eVUYC7ajVWUsZRqagxwRaa3VupjQ/Z2Z2v/Vffh/gJ/fFOS25s6mTyR2Lw1FrnBbRWo1iShR9BhekpPeQ=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.6.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-g8tR/yXtx08j1NMdaFsMy0caBFeTl6l4fbQWvyjKQJ5rUMf5oqV69iyrqwfl7tuD9N9cJo23yqpzrGmbYp8r3g=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-TrAgOcL63TRi7G92arTzq0n+VDrmZifwP1I1T9y2xU3lJpybsHdm33S2d3xaFfG0c8zJNIF9yYRqLSe6rbhH/A=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-XMhUiohsBJVwzJeS+w8y6E43I4rz/5ZpreSQAa6/gtNiXVBFhSw0inCKod5sJxuEETY2tTtK132lKcHVZAFgEQ=="], - "@smithy/util-retry": ["@smithy/util-retry@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-E/kFnvWQL6rIPr0Ucjk8oDgJSkKx2bv0nJkJ/cB3ywys7xCqeL1AXP9liHjgYONdQ+MKw/xT06IQK3vgbtu2Ww=="], + "@smithy/util-retry": ["@smithy/util-retry@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-l8i4lcA4AzvOc+aiMz8UyU7lSEgOmXd1Xktrhp7h1sO55j1VygpVUr/dAIfX9liY5HbDvDhTFZCgVHsYGlAoWw=="], - "@smithy/util-stream": ["@smithy/util-stream@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-g+hQ45sPnaIDU4CnaG8EufmeWwziQlcpIvPG6hVY7v65RcUgasM63J/WNfSsXEcZ1zFu9rS/r/qqfDxkIrQtDw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.7.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-lZfQFdsC48pRqCSv8R1jrjaAOJadldqH6ZbnaWgv9mKy77yYrMsqFam131hoa1obeydN2Qz52uUu+k9Og4W9sQ=="], - "@smithy/util-utf8": ["@smithy/util-utf8@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-tAa4sePYB7mlJzdYbdBqdv37KwFKWixmM/r3ihcI0HFOVjf+a5oGvtcLXcGm4S1bY4DFsLAIOHgjubtp+oRufw=="], + "@smithy/util-utf8": ["@smithy/util-utf8@4.4.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-dMvQY14daYwEfKR+/ACROrUwJ5onUue7d9o4KJo4gaecn5eVzxlCbSeU9GSh0ojFpIiI1bpnJJxO1wY2VXDEtQ=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-oTt3OP9NcJkrySCSCCdSbP6XLSMNgOmt/ulaiYtb0Ng6tfEWtXQ1mwfyqmLd+GapmDUjbU2mgkf7QIq9H4ij/g=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.5.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "tslib": "^2.6.2" } }, "sha512-BbTtz3ULP1na8PvteT1buTof7rUxcQd127FEjCT6jO99G2H3BR/OAlBRjWPZKJ9QvJPdYupR9/ai+rrnA8xneg=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], @@ -1554,35 +1553,35 @@ "@tabler/icons-react": ["@tabler/icons-react@3.44.0", "", { "dependencies": { "@tabler/icons": "3.44.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "postcss": "8.5.15", "tailwindcss": "4.3.1" } }, "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A=="], "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], @@ -1720,7 +1719,7 @@ "@types/html-to-text": ["@types/html-to-text@9.0.4", "", {}, "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ=="], - "@types/inquirer": ["@types/inquirer@8.2.12", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ=="], + "@types/inquirer": ["@types/inquirer@8.2.13", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-shSvl3mn4Z8AK627kA1vx8PYkyH6CdIjV5NYYj7a0xIxzmG3ZgzEpzCi3CWfktjAlq+0Z0wHJGtWNiACaYpeOg=="], "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], @@ -1738,7 +1737,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.19.20", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw=="], + "@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1788,21 +1787,21 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.8", "vitest": "4.1.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.9", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.9", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.9", "vitest": "4.1.9" }, "optionalPeers": ["@vitest/browser"] }, "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g=="], - "@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - "@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="], + "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], - "@vitest/runner": ["@vitest/runner@4.1.8", "", { "dependencies": { "@vitest/utils": "4.1.8", "pathe": "^2.0.3" } }, "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg=="], + "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - "@vitest/spy": ["@vitest/spy@4.1.8", "", {}, "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA=="], + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - "@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], + "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], "@webgpu/types": ["@webgpu/types@0.1.70", "", {}, "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA=="], @@ -1816,7 +1815,7 @@ "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -1826,7 +1825,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@5.0.197", "", { "dependencies": { "@ai-sdk/gateway": "2.0.98", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iUzFb2M3ZUL/Bbmfonh75DIZ354svWO5xh8VPC2wYNR6zzEMFghPOlJG5rtEpqRa037lHfdcjt0qmzg3em/WDw=="], + "ai": ["ai@5.0.203", "", { "dependencies": { "@ai-sdk/gateway": "2.0.102", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-U50l+Np0IDzkUJ59QOBb80y3x1QH8+XJqzoIIxkO756mPlOuoRfj80CFIBKtaHmDSTJoEAMdt0iaaDOqMCGZkQ=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -1860,7 +1859,7 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -1876,7 +1875,7 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], + "axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -1886,7 +1885,7 @@ "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.35", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], @@ -1908,7 +1907,7 @@ "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -1950,7 +1949,7 @@ "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], @@ -2230,7 +2229,7 @@ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - "dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="], + "dompurify": ["dompurify@3.4.10", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], @@ -2248,7 +2247,7 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "e2b": ["e2b@2.28.2", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^11.1.0", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.11", "undici": "^7.25.0" } }, "sha512-ZlP8Qw5SA0o+SLynqXugNwNoWMoQYyZWf8v/Z2oUSvzNxglH2SUQYcRCklscsH5WBsoB0X0biOh2S6P7LSWa8w=="], + "e2b": ["e2b@2.30.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "chalk": "^5.3.0", "compare-versions": "^6.1.0", "dockerfile-ast": "^0.7.1", "glob": "^11.1.0", "openapi-fetch": "^0.14.1", "platform": "^1.3.6", "tar": "^7.5.11", "undici": "^7.25.0" } }, "sha512-4kvfwh3QfPukrYmWEhrVLxL3WnQabzHabvhIRmvk6oU/YTWQtCrlZX+jaA9XBtVI/vQUbu5E5a6GlOhDXmcKzg=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -2260,7 +2259,7 @@ "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.370", "", {}, "sha512-D5tSHJReAb/Kf3Hu9F/GO4lJuSWzEWHwvQ/kKSUP7pimNgvxkSKj+gUQhHpKKACwrin7rS3byU7IxreF56rl5g=="], + "electron-to-chromium": ["electron-to-chromium@1.5.373", "", {}, "sha512-G2Hym8JIf/QreuseqkDibgH8Ci8KfJzqGDKdakbhSx9UltwRBH2cBLAWU/lBX0sCdv0TlhyxQyDCnSfxgMWsjA=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2280,7 +2279,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.23.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA=="], + "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], "entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], @@ -2380,7 +2379,7 @@ "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], - "fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="], + "fast-xml-parser": ["fast-xml-parser@5.9.0", "", { "dependencies": { "@nodable/entities": "^2.2.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^1.0.1", "path-expression-matcher": "^1.5.0", "strnum": "^2.4.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-duBuXbyIhEeNO4GjFuVqr0nF047oNwr18aum+zJyqo0MUG/n7Afgs3Qv3D6VN3ONedUKxiuFlPiMGIa0Z11chA=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -2408,7 +2407,7 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], @@ -2640,6 +2639,8 @@ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-unsafe": ["is-unsafe@1.0.1", "", {}, "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -3064,7 +3065,7 @@ "obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="], - "obug": ["obug@2.1.2", "", {}, "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg=="], + "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], "officeparser": ["officeparser@5.2.2", "", { "dependencies": { "@xmldom/xmldom": "^0.8.10", "concat-stream": "^2.0.0", "file-type": "^16.5.4", "node-ensure": "^0.0.0", "pdfjs-dist": "^5.3.31", "yauzl": "^3.1.3" }, "bin": { "officeparser": "officeParser.js" } }, "sha512-5JrV1CZFqTv/27fXy2bcf+3g6BpDZiJ3XoSRW3fb2i2EFex0DduqjTxiU2RsJ08WBsk4Hp0nZoGi9ZtHMZFaPA=="], @@ -3254,7 +3255,7 @@ "react-email": ["react-email@4.3.2", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.0", "ora": "^8.0.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tsconfig-paths": "4.2.0" }, "bin": { "email": "dist/index.js" } }, "sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA=="], - "react-hook-form": ["react-hook-form@7.78.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA=="], + "react-hook-form": ["react-hook-form@7.79.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-mhYp/MTmXvzYX6AJcJVko0rktoIhhmRnEouObj4wF5i/tCttgJvnp1+9wRkpITZjDTqpo4IOSJqu0dBlPlV/Lw=="], "react-pdf": ["react-pdf@10.4.1", "", { "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", "make-cancellable-promise": "^2.0.0", "make-event-props": "^2.0.0", "merge-refs": "^2.0.0", "pdfjs-dist": "5.4.296", "tiny-invariant": "^1.0.0", "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA=="], @@ -3408,7 +3409,7 @@ "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], - "semver": ["semver@7.8.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -3564,7 +3565,7 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], @@ -3602,7 +3603,7 @@ "tldts": ["tldts@7.0.30", "", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="], - "tldts-core": ["tldts-core@7.4.2", "", {}, "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA=="], + "tldts-core": ["tldts-core@7.4.3", "", {}, "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -3620,7 +3621,7 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -3722,7 +3723,7 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="], + "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], @@ -3814,8 +3815,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -3824,7 +3823,7 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1065.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.19", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-qdHQntq82gMqG6Tf8xrgmhJxacaYkxW4PEeDg/ISMVJ84EWe7iD6JyCTgbyox3uNDH6vqEJ8GUiTaXCq307zVw=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1069.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ks4X+kngC3PA5howV7Qu1TgG4bfC4jPykKdvw3nmBSXR9yZxRJouBholFSNQ5kY3L+Fgwyw+LCjzQmNi+KR91g=="], "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], @@ -3858,7 +3857,7 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@grpc/proto-loader/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], + "@grpc/proto-loader/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -3868,7 +3867,67 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], @@ -3876,9 +3935,9 @@ "@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], @@ -3886,7 +3945,7 @@ "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], @@ -3894,9 +3953,9 @@ "@radix-ui/react-navigation-menu/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.5", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], - "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], @@ -3906,7 +3965,7 @@ "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], @@ -3914,13 +3973,13 @@ "@radix-ui/react-scroll-area/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-select/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-select/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.5", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg=="], + "@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], "@radix-ui/react-slider/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], @@ -3968,13 +4027,13 @@ "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], @@ -4030,13 +4089,13 @@ "@types/busboy/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "@types/cors/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/cors/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@types/fluent-ffmpeg/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], "@types/jsdom/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "@types/node-fetch/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/node-fetch/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@types/nodemailer/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], @@ -4044,9 +4103,9 @@ "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "@types/through/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/through/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], - "@types/ws/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/ws/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4064,15 +4123,17 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "c12/confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], - "c12/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "chrome-launcher/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "chrome-launcher/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -4080,8 +4141,6 @@ "cmdk/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "cmdk/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4098,7 +4157,7 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "docx/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "docx/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "docx/nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], @@ -4110,7 +4169,7 @@ "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "engine.io/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "engine.io/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], @@ -4132,27 +4191,21 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "fumadocs-core/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "fumadocs-core/shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], - "fumadocs-mdx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], - - "fumadocs-mdx/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "fumadocs-openapi/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "fumadocs-openapi/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "fumadocs-openapi/lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "fumadocs-openapi/lucide-react": ["lucide-react@1.18.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA=="], "fumadocs-openapi/shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], - "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], - "fumadocs-ui/lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "fumadocs-ui/lucide-react": ["lucide-react@1.18.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA=="], "fumadocs-ui/shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], @@ -4184,8 +4237,6 @@ "isomorphic-unfetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "json-schema-to-typescript/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "libmime/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -4252,7 +4303,7 @@ "pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], - "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], "posthog-js/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], @@ -4262,7 +4313,7 @@ "posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], - "protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -4300,11 +4351,11 @@ "sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], - "simstudio/@types/node": ["@types/node@20.19.42", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg=="], + "simstudio/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], "simstudio/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "simstudio-ts-sdk/@types/node": ["@types/node@20.19.42", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg=="], + "simstudio-ts-sdk/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -4334,7 +4385,7 @@ "tough-cookie/tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], - "tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "twilio/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -4416,7 +4467,7 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@grpc/proto-loader/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@grpc/proto-loader/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -4530,57 +4581,57 @@ "fumadocs-core/shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], - "fumadocs-mdx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + "fumadocs-mdx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "fumadocs-mdx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + "fumadocs-mdx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "fumadocs-mdx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + "fumadocs-mdx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "fumadocs-mdx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + "fumadocs-mdx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "fumadocs-mdx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + "fumadocs-mdx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "fumadocs-mdx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + "fumadocs-mdx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "fumadocs-mdx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + "fumadocs-mdx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "fumadocs-mdx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + "fumadocs-mdx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + "fumadocs-mdx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + "fumadocs-mdx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "fumadocs-mdx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + "fumadocs-mdx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "fumadocs-mdx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + "fumadocs-mdx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "fumadocs-mdx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + "fumadocs-mdx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + "fumadocs-mdx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + "fumadocs-mdx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "fumadocs-mdx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + "fumadocs-mdx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "fumadocs-mdx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + "fumadocs-mdx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "fumadocs-mdx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + "fumadocs-mdx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "fumadocs-mdx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + "fumadocs-mdx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "fumadocs-mdx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + "fumadocs-mdx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "fumadocs-mdx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + "fumadocs-mdx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "fumadocs-mdx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + "fumadocs-mdx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "fumadocs-mdx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + "fumadocs-mdx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "fumadocs-mdx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + "fumadocs-mdx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "fumadocs-mdx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + "fumadocs-mdx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "fumadocs-mdx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + "fumadocs-mdx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "fumadocs-openapi/shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], @@ -4728,63 +4779,63 @@ "sim/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "sim/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "sim/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "tough-cookie/tldts/tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "twilio/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -4806,11 +4857,11 @@ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], + "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], - "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], + "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], - "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], + "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], "@trigger.dev/core/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], @@ -4818,7 +4869,7 @@ "@trigger.dev/core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="], - "@trigger.dev/core/socket.io/engine.io/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@trigger.dev/core/socket.io/engine.io/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@trigger.dev/core/socket.io/engine.io/cookie": ["cookie@0.4.2", "", {}, "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="], @@ -4878,7 +4929,7 @@ "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="], + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], "rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -4892,11 +4943,11 @@ "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], - "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], - "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@trigger.dev/core/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@trigger.dev/core/socket.io/engine.io/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -4914,7 +4965,7 @@ "log-update/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], From 6cbaf4282d0baae858527452bfc84d0c05467e36 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:11:19 -0700 Subject: [PATCH 24/24] fix(scheduled-tasks): fix scheduled tasks schema validation (#5091) * fix(scheduled-tasks): fix schema rejection * fix(db): fix duplicate db query --- apps/sim/app/api/schedules/[id]/route.ts | 55 ++++++++++++------- .../resource-content/resource-content.tsx | 10 +--- apps/sim/hooks/queries/schedules.ts | 26 +++++++++ apps/sim/lib/api/contracts/schedules.ts | 17 ++++++ apps/sim/lib/copilot/chat/post.ts | 4 ++ apps/sim/lib/copilot/chat/process-contents.ts | 50 ++++++++++++++++- 6 files changed, 132 insertions(+), 30 deletions(-) diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 8e6e575c074..62455bb85d4 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -9,7 +9,7 @@ import { } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { updateScheduleContract } from '@/lib/api/contracts/schedules' +import { getScheduleByIdContract, updateScheduleContract } from '@/lib/api/contracts/schedules' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' @@ -27,16 +27,7 @@ const logger = createLogger('ScheduleAPI') export const dynamic = 'force-dynamic' -type ScheduleRow = { - id: string - workflowId: string | null - status: string - cronExpression: string | null - timezone: string | null - sourceType: string | null - sourceWorkspaceId: string | null - jobTitle: string | null -} +type ScheduleRow = typeof workflowSchedule.$inferSelect async function fetchAndAuthorize( requestId: string, @@ -45,16 +36,7 @@ async function fetchAndAuthorize( action: 'read' | 'write' ): Promise<{ schedule: ScheduleRow; workspaceId: string | null } | NextResponse> { const [schedule] = await db - .select({ - id: workflowSchedule.id, - workflowId: workflowSchedule.workflowId, - status: workflowSchedule.status, - cronExpression: workflowSchedule.cronExpression, - timezone: workflowSchedule.timezone, - sourceType: workflowSchedule.sourceType, - sourceWorkspaceId: workflowSchedule.sourceWorkspaceId, - jobTitle: workflowSchedule.jobTitle, - }) + .select() .from(workflowSchedule) .where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt))) .limit(1) @@ -103,6 +85,37 @@ async function fetchAndAuthorize( return { schedule, workspaceId: authorization.workflow.workspaceId ?? null } } +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const requestId = generateRequestId() + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(getScheduleByIdContract, request, context, { + validationErrorResponse: () => + NextResponse.json({ error: 'Invalid request' }, { status: 400 }), + }) + if (!parsed.success) return parsed.response + + const { id: scheduleId } = parsed.data.params + + // fetchAndAuthorize already loads the full row (and 404s if missing), so + // return it directly — no second query. + const authResult = await fetchAndAuthorize(requestId, scheduleId, session.user.id, 'read') + if (authResult instanceof NextResponse) return authResult + + return NextResponse.json({ schedule: authResult.schedule }) + } catch (error) { + logger.error(`[${requestId}] Failed to get schedule`, { error }) + return NextResponse.json({ error: 'Failed to get schedule' }, { status: 500 }) + } + } +) + export const PUT = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 55219f75006..050aa22b3d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -53,7 +53,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import { useFolders } from '@/hooks/queries/folders' import { useLogDetail } from '@/hooks/queries/logs' -import { useWorkspaceSchedules } from '@/hooks/queries/schedules' +import { useScheduleById } from '@/hooks/queries/schedules' import { downloadTableExport } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' @@ -693,12 +693,8 @@ interface EmbeddedScheduledTaskProps { scheduleId: string } -function EmbeddedScheduledTask({ workspaceId, scheduleId }: EmbeddedScheduledTaskProps) { - const { data: schedules = [], isLoading, isError } = useWorkspaceSchedules(workspaceId) - const schedule = useMemo( - () => schedules.find((s) => s.id === scheduleId), - [schedules, scheduleId] - ) +function EmbeddedScheduledTask({ scheduleId }: EmbeddedScheduledTaskProps) { + const { data: schedule, isLoading, isError } = useScheduleById(scheduleId) if (isLoading && !schedule) return LOADING_SKELETON diff --git a/apps/sim/hooks/queries/schedules.ts b/apps/sim/hooks/queries/schedules.ts index 42108faf8ee..c3f035a0d59 100644 --- a/apps/sim/hooks/queries/schedules.ts +++ b/apps/sim/hooks/queries/schedules.ts @@ -11,6 +11,7 @@ import { deleteScheduleContract, disableScheduleContract, excludeOccurrenceContract, + getScheduleByIdContract, getScheduleContract, listWorkspaceSchedulesContract, reactivateScheduleContract, @@ -31,6 +32,7 @@ export const scheduleKeys = { details: () => [...scheduleKeys.all, 'detail'] as const, schedule: (workflowId: string, blockId: string) => [...scheduleKeys.details(), workflowId, blockId] as const, + byId: (scheduleId: string) => [...scheduleKeys.details(), scheduleId] as const, } export type ScheduleData = WorkflowScheduleRow @@ -88,6 +90,30 @@ export function useWorkspaceSchedules(workspaceId?: string) { }) } +/** + * Fetch a single schedule (job) by id. Used by the mothership resource viewer so + * opening a scheduled-task artifact does a lightweight by-id read instead of the + * whole-workspace `useWorkspaceSchedules` fetch (which contended with the chat + * stream connection and stalled start/resume). + */ +export function useScheduleById(scheduleId?: string) { + return useQuery({ + queryKey: scheduleKeys.byId(scheduleId ?? ''), + queryFn: async ({ signal }) => { + if (!scheduleId) throw new Error('Schedule ID required') + + const data = await requestJson(getScheduleByIdContract, { + params: { id: scheduleId }, + signal, + }) + return data.schedule + }, + enabled: Boolean(scheduleId), + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} + /** * Hook to fetch schedule data for a workflow block */ diff --git a/apps/sim/lib/api/contracts/schedules.ts b/apps/sim/lib/api/contracts/schedules.ts index ca48bce7643..d4eaf3d5e11 100644 --- a/apps/sim/lib/api/contracts/schedules.ts +++ b/apps/sim/lib/api/contracts/schedules.ts @@ -216,6 +216,23 @@ export const listWorkspaceSchedulesContract = defineRouteContract({ }, }) +/** + * Single-schedule read by id. Used by the mothership resource viewer so opening + * a scheduled-task artifact does a lightweight by-id fetch instead of pulling + * the entire workspace schedule list (which contended with the chat stream). + */ +export const getScheduleByIdContract = defineRouteContract({ + method: 'GET', + path: '/api/schedules/[id]', + params: scheduleIdParamsSchema, + response: { + mode: 'json', + schema: z.object({ + schedule: workflowScheduleRowSchema, + }), + }, +}) + /** * Newly-created job schedules emit a partial summary with the canonical fields * the route synthesizes server-side; everything else is filled in on diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 721aa8b5519..e1347d05316 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -75,6 +75,7 @@ const ResourceAttachmentSchema = z.object({ 'filefolder', 'task', 'log', + 'scheduledtask', 'generic', ]), id: z.string().min(1), @@ -91,6 +92,7 @@ const GENERIC_RESOURCE_TITLE: Record['t filefolder: 'File Folder', task: 'Task', log: 'Log', + scheduledtask: 'Scheduled Task', generic: 'Resource', } @@ -108,6 +110,7 @@ const ChatContextSchema = z.object({ 'file', 'folder', 'filefolder', + 'scheduledtask', 'integration', 'skill', ]), @@ -123,6 +126,7 @@ const ChatContextSchema = z.object({ folderId: z.string().optional(), fileFolderId: z.string().optional(), skillId: z.string().optional(), + scheduleId: z.string().optional(), }) const ChatMessageSchema = z.object({ diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 81f4eae47c2..3edafe1ca93 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -1,11 +1,12 @@ import { db, dbReplica } from '@sim/db' -import { knowledgeBase } from '@sim/db/schema' +import { knowledgeBase, workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission, getActiveWorkflowRecord, } from '@sim/workflow-authz' -import { and, eq, isNull } from 'drizzle-orm' +import { and, eq, isNull, ne } from 'drizzle-orm' +import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import { buildVfsFolderPathMap, canonicalBlockVfsPath, @@ -168,6 +169,16 @@ export async function processContextsServer( path: result.path, } } + if (ctx.kind === 'scheduledtask' && ctx.scheduleId && currentWorkspaceId) { + const result = await resolveScheduledTaskResource(ctx.scheduleId, currentWorkspaceId) + if (!result) return null + return { + type: 'active_resource', + tag: ctx.label ? `@${ctx.label}` : '@', + content: result.content, + path: result.path, + } + } if (ctx.kind === 'docs') { try { const { searchDocumentationServerTool } = await import( @@ -695,6 +706,9 @@ export async function resolveActiveResourceContext( case 'filefolder': { return await resolveFileFolderResource(resourceId, workspaceId) } + case 'scheduledtask': { + return await resolveScheduledTaskResource(resourceId, workspaceId) + } default: return null } @@ -718,6 +732,38 @@ async function resolveTableResource( } } +async function resolveScheduledTaskResource( + scheduleId: string, + workspaceId: string +): Promise { + const [row] = await db + .select({ id: workflowSchedule.id, jobTitle: workflowSchedule.jobTitle }) + .from(workflowSchedule) + .where( + and( + eq(workflowSchedule.id, scheduleId), + eq(workflowSchedule.sourceWorkspaceId, workspaceId), + eq(workflowSchedule.sourceType, 'job'), + isNull(workflowSchedule.archivedAt), + // Mirror the VFS materializer (workspace-vfs `materializeJobs`), which + // excludes completed jobs — otherwise we'd point at a meta.json it never + // wrote and the agent's read would dangle. + ne(workflowSchedule.status, 'completed') + ) + ) + .limit(1) + if (!row) return null + // The VFS materializes jobs at `jobs/{sanitized title}/meta.json` (see + // workspace-vfs `materializeJobs`); emit the same lightweight path pointer so + // the agent reads it via the VFS instead of us inlining the (heavy) row. + return { + type: 'active_resource', + tag: '@active_resource', + content: '', + path: `jobs/${normalizeVfsSegment(row.jobTitle || row.id)}/meta.json`, + } +} + async function resolveFileResource( fileId: string, workspaceId: string