diff --git a/apps/sim/app/api/workflows/[id]/deployed/route.test.ts b/apps/sim/app/api/workflows/[id]/deployed/route.test.ts index 374b99edaa5..fe903c3a19f 100644 --- a/apps/sim/app/api/workflows/[id]/deployed/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/deployed/route.test.ts @@ -49,6 +49,7 @@ const DEPLOYED_STATE = { loops: {}, parallels: {}, variables: {}, + deploymentVersionId: 'deployment-version-1', } const SESSION = { diff --git a/apps/sim/app/api/workflows/[id]/deployed/route.ts b/apps/sim/app/api/workflows/[id]/deployed/route.ts index 1df1cefeb2c..6d806999422 100644 --- a/apps/sim/app/api/workflows/[id]/deployed/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployed/route.ts @@ -26,17 +26,23 @@ export const GET = defineInternalJsonRoute({ errorPolicy: internalOrchestrationErrorPolicy, mapInput: ({ params }) => ({ workflowId: params.id, state: 'deployed' as const }), useCase: readWorkflowDefinition, - present: ({ state }) => ({ - deployedState: state - ? deployedWorkflowStateSchema.parse({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - parallels: state.parallels, - variables: 'variables' in state ? (state.variables ?? {}) : {}, - }) - : null, - }), + present: ({ state }) => { + if (state && (!('deploymentVersionId' in state) || !state.deploymentVersionId)) { + throw new Error('Deployed workflow state is missing its deployment version') + } + return { + deployedState: state + ? deployedWorkflowStateSchema.parse({ + blocks: state.blocks, + edges: state.edges, + loops: state.loops, + parallels: state.parallels, + variables: 'variables' in state ? (state.variables ?? {}) : {}, + deploymentVersionId: state.deploymentVersionId, + }) + : null, + } + }, onSuccess: ({ input, result }) => { if (!result.state) logger.warn('Workflow has no active deployed state', input) }, diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts new file mode 100644 index 00000000000..2865ca324f8 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts @@ -0,0 +1,172 @@ +/** + * @vitest-environment node + */ + +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + getSession: vi.fn(), + read: vi.fn(), + update: vi.fn(), +})) + +vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession })) + +vi.mock('@/lib/credential-groups/application/manage-access', () => ({ + readCredentialGroupAccess: { + operation: { id: 'credential_groups.access.read' }, + execute: mocks.read, + }, + updateCredentialGroupAccess: { + operation: { id: 'credential_groups.access.update' }, + execute: mocks.update, + }, +})) + +import { GET, PUT } from '@/app/api/workspaces/[id]/credential-groups/[groupId]/access/route' + +const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111' +const GROUP_ID = 'group-1' +const url = `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/credential-groups/${GROUP_ID}/access` +const context = { params: Promise.resolve({ id: WORKSPACE_ID, groupId: GROUP_ID }) } +const workflows = [ + { id: 'workflow-1', name: 'Support workflow' }, + { id: 'workflow-2', name: 'Finance workflow' }, +] + +describe('Credential Group access route', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.getSession.mockResolvedValue({ + user: { id: 'admin-1' }, + session: { id: 'session-1' }, + }) + mocks.read.mockResolvedValue({ + revision: 1, + allowedWorkflowIds: ['workflow-1'], + workflows, + }) + mocks.update.mockResolvedValue({ + revision: 2, + allowedWorkflowIds: ['workflow-1', 'workflow-2'], + }) + }) + + it('reads the workflow-only access selection and bounded catalog', async () => { + const request = new NextRequest(url) + const response = await GET(request, context) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + revision: 1, + allowedWorkflowIds: ['workflow-1'], + workflows, + }) + expect(mocks.read).toHaveBeenCalledWith({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { assertedWorkspaceId: WORKSPACE_ID, credentialGroupId: GROUP_ID }, + request, + }) + }) + + it('updates selected workflows with optimistic revision input', async () => { + const body = { + expectedRevision: 1, + allowedWorkflowIds: ['workflow-2', 'workflow-1'], + } + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify(body), + headers: { 'content-type': 'application/json' }, + }) + const response = await PUT(request, context) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + revision: 2, + allowedWorkflowIds: ['workflow-1', 'workflow-2'], + }) + expect(mocks.update).toHaveBeenCalledWith({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { + assertedWorkspaceId: WORKSPACE_ID, + credentialGroupId: GROUP_ID, + ...body, + }, + request, + }) + }) + + it('authenticates before parsing a malformed body', async () => { + mocks.getSession.mockResolvedValue(null) + const request = new NextRequest(url, { + method: 'PUT', + body: '{', + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(401) + expect(mocks.update).not.toHaveBeenCalled() + }) + + it('rejects the removed raw resource-policy document wire shape', async () => { + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify({ + expectedRevision: 1, + allowedWorkflowIds: [], + document: { + version: 1, + resource: { type: 'credential_group', id: GROUP_ID }, + statements: [], + }, + }), + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(400) + expect(mocks.update).not.toHaveBeenCalled() + }) + + it('rejects duplicate workflow selections at the HTTP boundary', async () => { + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify({ + expectedRevision: 1, + allowedWorkflowIds: ['workflow-1', 'workflow-1'], + }), + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(400) + expect(mocks.update).not.toHaveBeenCalled() + }) + + it('rejects an oversized access payload before parsing it', async () => { + const body = JSON.stringify({ + expectedRevision: 1, + allowedWorkflowIds: [], + padding: 'x'.repeat(40_000), + }) + const request = new NextRequest(url, { + method: 'PUT', + body, + headers: { + 'content-length': String(Buffer.byteLength(body)), + 'content-type': 'application/json', + }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(413) + expect(mocks.update).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts new file mode 100644 index 00000000000..18c61a78066 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts @@ -0,0 +1,49 @@ +import { + getCredentialGroupAccessContract, + updateCredentialGroupAccessContract, +} from '@/lib/api/contracts/credential-groups' +import { + defineInternalJsonRoute, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { + readCredentialGroupAccess, + updateCredentialGroupAccess, +} from '@/lib/credential-groups/application/manage-access' +import { credentialGroupOperations } from '@/lib/credential-groups/application/operations' +import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[id]/credential-groups/error-policy' + +const rateLimit = internalRateLimits.none({ + reason: 'Credential Group access changes are workspace-admin control-plane operations', +}) +const MAX_CREDENTIAL_GROUP_ACCESS_BODY_BYTES = 32 * 1024 + +export const GET = defineInternalJsonRoute({ + contract: getCredentialGroupAccessContract, + auth: internalSessionAuth, + operation: credentialGroupOperations.readAccess, + rateLimit, + errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to read Credential Group access'), + mapInput: ({ params }) => ({ + assertedWorkspaceId: params.id, + credentialGroupId: params.groupId, + }), + useCase: readCredentialGroupAccess, +}) + +export const PUT = defineInternalJsonRoute({ + contract: updateCredentialGroupAccessContract, + auth: internalSessionAuth, + operation: credentialGroupOperations.updateAccess, + rateLimit, + errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to update Credential Group access'), + parseOptions: { maxBodyBytes: MAX_CREDENTIAL_GROUP_ACCESS_BODY_BYTES }, + mapInput: ({ params, body }) => ({ + assertedWorkspaceId: params.id, + credentialGroupId: params.groupId, + expectedRevision: body.expectedRevision, + allowedWorkflowIds: body.allowedWorkflowIds, + }), + useCase: updateCredentialGroupAccess, +}) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts index aeb354f260d..c95046d925f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts @@ -107,7 +107,7 @@ export const credentialGroupIdUrlKeys = { /** Active view inside a credential-group detail page. */ export const credentialGroupTabParam = { key: 'credential-group-tab', - parser: parseAsStringLiteral(['details', 'people'] as const).withDefault('details'), + parser: parseAsStringLiteral(['details', 'people', 'access'] as const).withDefault('details'), } as const /** Tab view-state: clean URLs, no back-stack churn. */ diff --git a/apps/sim/components/settings/use-settings-unsaved-guard.ts b/apps/sim/components/settings/use-settings-unsaved-guard.ts index 0fbf43786d4..a9f272a652d 100644 --- a/apps/sim/components/settings/use-settings-unsaved-guard.ts +++ b/apps/sim/components/settings/use-settings-unsaved-guard.ts @@ -3,6 +3,7 @@ import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' interface UseSettingsUnsavedGuardParams { isDirty: boolean + navigationBlocked?: boolean } interface SettingsUnsavedGuard { @@ -17,27 +18,40 @@ interface SettingsUnsavedGuard { */ export function useSettingsUnsavedGuard({ isDirty, + navigationBlocked = false, }: UseSettingsUnsavedGuardParams): SettingsUnsavedGuard { const setDirty = useSettingsDirtyStore((state) => state.setDirty) + const setNavigationBlocked = useSettingsDirtyStore((state) => state.setNavigationBlocked) const reset = useSettingsDirtyStore((state) => state.reset) const isDirtyRef = useRef(isDirty) + const navigationBlockedRef = useRef(navigationBlocked) const pendingLeaveRef = useRef<(() => void) | null>(null) const [showUnsavedModal, setShowUnsavedModal] = useState(false) useEffect(() => { isDirtyRef.current = isDirty + navigationBlockedRef.current = navigationBlocked setDirty(isDirty) + setNavigationBlocked(navigationBlocked) + if (navigationBlocked) { + pendingLeaveRef.current = null + setShowUnsavedModal(false) + return + } if (!isDirty) { pendingLeaveRef.current = null setShowUnsavedModal(false) } - }, [isDirty, setDirty]) + }, [isDirty, navigationBlocked, setDirty, setNavigationBlocked]) useEffect(() => { return () => reset() }, [reset]) const guardBack = useCallback((onLeave: () => void) => { + if (navigationBlockedRef.current || useSettingsDirtyStore.getState().navigationBlocked) { + return + } if (isDirtyRef.current) { pendingLeaveRef.current = onLeave setShowUnsavedModal(true) @@ -47,6 +61,9 @@ export function useSettingsUnsavedGuard({ }, []) const confirmDiscard = useCallback(() => { + if (navigationBlockedRef.current || useSettingsDirtyStore.getState().navigationBlocked) { + return + } setShowUnsavedModal(false) pendingLeaveRef.current?.() pendingLeaveRef.current = null diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx new file mode 100644 index 00000000000..27fe230e9a9 --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx @@ -0,0 +1,377 @@ +/** + * @vitest-environment jsdom + */ + +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' + +const mocks = vi.hoisted(() => ({ + addWorkflowModalProps: null as { + workflows: Array<{ id: string; name: string }> + onAdd: (workflowId: string) => void + onClose: () => void + } | null, + mutationError: null as Error | null, + mutateAsync: vi.fn(), + reset: vi.fn(), + toastError: vi.fn(), + toastSuccess: vi.fn(), + useAccess: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ + Chip: ({ + children, + onClick, + disabled, + }: { + children: ReactNode + onClick?: () => void + disabled?: boolean + }) => ( + + ), + toast: { error: mocks.toastError, success: mocks.toastSuccess }, +})) + +vi.mock('@sim/emcn/icons', () => ({ Plus: () => null, Workflow: () => null })) + +vi.mock('@/hooks/queries/credential-groups', () => ({ + useCredentialGroupAccess: mocks.useAccess, + useUpdateCredentialGroupAccess: () => ({ + error: mocks.mutationError, + isPending: false, + mutateAsync: mocks.mutateAsync, + reset: mocks.reset, + }), +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/row-actions-menu', () => ({ + RowActionsMenu: ({ actions }: { actions: Array<{ label: string; onSelect: () => void }> }) => ( +
+ {actions.map((action) => ( + + ))} +
+ ), +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({ + SettingsEmptyState: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-resource-row', () => ({ + RESOURCE_LIST_STACK: '', + SettingsResourceRow: ({ + title, + description, + trailing, + }: { + title: ReactNode + description?: ReactNode + trailing?: ReactNode + }) => ( +
+ {title} + {description} + {trailing} +
+ ), +})) + +vi.mock( + '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section', + () => ({ + SettingsSection: ({ + label, + action, + children, + }: { + label: ReactNode + action?: ReactNode + children: ReactNode + }) => ( +
+

{label}

+ {action} + {children} +
+ ), + }) +) + +vi.mock('@/ee/credential-groups/components/credential-group-add-workflow-modal', () => ({ + CredentialGroupAddWorkflowModal: (props: { + workflows: Array<{ id: string; name: string }> + onAdd: (workflowId: string) => void + onClose: () => void + }) => { + mocks.addWorkflowModalProps = props + return
Add workflow modal
+ }, +})) + +import { + CredentialGroupAccess, + useCredentialGroupAccessEditor, +} from '@/ee/credential-groups/components/credential-group-access' + +const GROUP_ID = 'group-1' +const WORKFLOWS = [ + { id: 'workflow-1', name: 'Finance workflow' }, + { id: 'workflow-2', name: 'Support workflow' }, +] +const mountedRoots: Root[] = [] + +function renderHook() { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + let result: ReturnType | undefined + + function Probe() { + result = useCredentialGroupAccessEditor({ + workspaceId: 'workspace-1', + groupId: GROUP_ID, + enabled: true, + }) + return null + } + + const rerender = () => { + act(() => root.render()) + } + rerender() + + return { + getResult: () => { + if (!result) throw new Error('Access editor hook did not render') + return result + }, + rerender, + } +} + +function renderAccess(overrides: Partial> = {}) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + const onAllowedWorkflowIdsChange = vi.fn() + act(() => + root.render( + + ) + ) + const button = (label: string) => { + const match = [...container.querySelectorAll('button')].find( + (candidate) => candidate.textContent === label + ) + if (!(match instanceof HTMLButtonElement)) throw new Error(`Button ${label} not found`) + return match + } + return { button, container, onAllowedWorkflowIdsChange } +} + +beforeEach(() => { + vi.clearAllMocks() + useSettingsDirtyStore.getState().reset() + mocks.addWorkflowModalProps = null + mocks.mutationError = null + mocks.useAccess.mockReturnValue({ + data: { + revision: 3, + allowedWorkflowIds: ['workflow-1'], + workflows: WORKFLOWS, + }, + error: null, + isPending: false, + }) + mocks.mutateAsync.mockResolvedValue({ revision: 4, allowedWorkflowIds: ['workflow-2'] }) +}) + +afterEach(() => { + act(() => { + for (const root of mountedRoots.splice(0)) root.unmount() + }) +}) + +describe('Credential Group access editor', () => { + it('stages normalized workflow access against the loaded revision', () => { + const editor = renderHook() + + expect(editor.getResult().allowedWorkflowIds).toEqual(['workflow-1']) + expect(editor.getResult().revision).toBe(3) + expect(editor.getResult().dirty).toBe(false) + + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-2', 'workflow-1'], 3)) + + expect(editor.getResult().allowedWorkflowIds).toEqual(['workflow-1', 'workflow-2']) + expect(editor.getResult().revision).toBe(3) + expect(editor.getResult().dirty).toBe(true) + expect(mocks.mutateAsync).not.toHaveBeenCalled() + }) + + it('saves the staged workflow IDs and clears the draft', async () => { + const editor = renderHook() + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-2'], 3)) + + await act(async () => editor.getResult().save()) + + expect(mocks.mutateAsync).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + groupId: GROUP_ID, + body: { expectedRevision: 3, allowedWorkflowIds: ['workflow-2'] }, + }) + expect(editor.getResult().dirty).toBe(false) + expect(mocks.toastSuccess).toHaveBeenCalledWith('Workflow access saved') + }) + + it('blocks settings navigation until the save request settles', async () => { + let resolveSave: ((value: { revision: number; allowedWorkflowIds: string[] }) => void) | null = + null + mocks.mutateAsync.mockReturnValue( + new Promise((resolve) => { + resolveSave = resolve + }) + ) + const editor = renderHook() + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-2'], 3)) + + let savePromise: Promise | undefined + act(() => { + savePromise = editor.getResult().save() + }) + expect(useSettingsDirtyStore.getState().navigationBlocked).toBe(true) + + await act(async () => { + if (!resolveSave) throw new Error('Save resolver is unavailable') + resolveSave({ revision: 4, allowedWorkflowIds: ['workflow-2'] }) + await savePromise + }) + expect(useSettingsDirtyStore.getState().navigationBlocked).toBe(false) + }) + + it('preserves the pinned draft when a concurrent update conflicts', async () => { + const editor = renderHook() + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-2'], 3)) + mocks.useAccess.mockReturnValue({ + data: { revision: 4, allowedWorkflowIds: [], workflows: WORKFLOWS }, + error: null, + isPending: false, + }) + const conflict = new Error('Credential Group workflow access changed while it was edited') + mocks.mutateAsync.mockImplementation(async () => { + mocks.mutationError = conflict + throw conflict + }) + editor.rerender() + + await act(async () => editor.getResult().save()) + editor.rerender() + + expect(mocks.mutateAsync).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + groupId: GROUP_ID, + body: { expectedRevision: 3, allowedWorkflowIds: ['workflow-2'] }, + }) + expect(editor.getResult().allowedWorkflowIds).toEqual(['workflow-2']) + expect(editor.getResult().revision).toBe(3) + expect(editor.getResult().dirty).toBe(true) + expect(editor.getResult().error).toBe(conflict.message) + }) + + it('discards staged workflow access back to the query value', () => { + const editor = renderHook() + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-2'], 3)) + + act(() => editor.getResult().discard()) + + expect(editor.getResult().allowedWorkflowIds).toEqual(['workflow-1']) + expect(editor.getResult().dirty).toBe(false) + }) + + it('fails fast on duplicate workflow access', () => { + const editor = renderHook() + + expect(() => + act(() => editor.getResult().setAllowedWorkflowIds(['workflow-1', 'workflow-1'], 3)) + ).toThrow('contains duplicate workflows') + }) + + it('fails fast instead of normalizing a non-canonical workflow ID', () => { + const editor = renderHook() + + expect(() => act(() => editor.getResult().setAllowedWorkflowIds([' workflow-1'], 3))).toThrow( + 'requires canonical non-empty workflow IDs' + ) + }) +}) + +describe('CredentialGroupAccess', () => { + it('renders named workflow rows and stages removal', () => { + const access = renderAccess() + + expect(access.container.textContent).toContain('Workflow access') + expect(access.container.textContent).toContain('Finance workflow') + expect(access.container.textContent).toContain( + 'Deployed runs can use every credential in this group' + ) + + act(() => access.button('Remove').click()) + + expect(access.onAllowedWorkflowIdsChange).toHaveBeenCalledWith([], 7) + }) + + it('opens the picker with only workflows that do not have access', () => { + const access = renderAccess() + + act(() => access.button('Add workflow').click()) + + expect(mocks.addWorkflowModalProps?.workflows).toEqual([ + { id: 'workflow-2', name: 'Support workflow' }, + ]) + act(() => mocks.addWorkflowModalProps?.onAdd('workflow-2')) + expect(access.onAllowedWorkflowIdsChange).toHaveBeenCalledWith(['workflow-1', 'workflow-2'], 7) + }) + + it('renders allowed workflows in canonical catalog order', () => { + const access = renderAccess({ allowedWorkflowIds: ['workflow-2', 'workflow-1'] }) + const text = access.container.textContent ?? '' + + expect(text.indexOf('Finance workflow')).toBeLessThan(text.indexOf('Support workflow')) + }) + + it('fails fast when selected access references an unavailable workflow', () => { + expect(() => renderAccess({ allowedWorkflowIds: ['deleted-workflow-id'] })).toThrow( + 'references unavailable workflow deleted-workflow-id' + ) + }) + + it('renders the empty and error states without the generic policy editor', () => { + const empty = renderAccess({ allowedWorkflowIds: [] }) + expect(empty.container.textContent).toContain('No workflows have access') + expect(empty.container.textContent).not.toContain('Access policy') + + const failed = renderAccess({ loadError: new Error('Access request failed') }) + expect(failed.container.textContent).toContain('Access request failed') + }) +}) diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.tsx new file mode 100644 index 00000000000..ffee79b853c --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.tsx @@ -0,0 +1,286 @@ +'use client' + +import { useState } from 'react' +import { Chip, toast } from '@sim/emcn' +import { Workflow } from '@sim/emcn/icons' +import { getErrorMessage } from '@sim/utils/errors' +import type { CredentialGroupAccessResponse } from '@/lib/api/contracts/credential-groups' +import { CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT } from '@/lib/credential-groups/workflow-access-limits' +import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' +import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { + RESOURCE_LIST_STACK, + SettingsResourceRow, +} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { CredentialGroupAddWorkflowModal } from '@/ee/credential-groups/components/credential-group-add-workflow-modal' +import { + useCredentialGroupAccess, + useUpdateCredentialGroupAccess, +} from '@/hooks/queries/credential-groups' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' + +interface AccessDraft { + allowedWorkflowIds: string[] + baseline: string + expectedRevision: number + groupId: string +} + +interface UseCredentialGroupAccessEditorProps { + workspaceId: string + groupId: string + enabled: boolean +} + +function normalizeAllowedWorkflowIds(workflowIds: readonly string[]): string[] { + for (const workflowId of workflowIds) { + if (!workflowId || workflowId !== workflowId.trim()) { + throw new Error('Credential Group workflow access requires canonical non-empty workflow IDs') + } + } + if (new Set(workflowIds).size !== workflowIds.length) { + throw new Error('Credential Group workflow access contains duplicate workflows') + } + if (workflowIds.length > CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT) { + throw new Error( + `Credential Group workflow access cannot exceed ${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} workflows` + ) + } + return [...workflowIds].sort() +} + +function serializeAllowedWorkflowIds(workflowIds: readonly string[]): string { + return JSON.stringify(normalizeAllowedWorkflowIds(workflowIds)) +} + +export function useCredentialGroupAccessEditor({ + workspaceId, + groupId, + enabled, +}: UseCredentialGroupAccessEditorProps) { + const access = useCredentialGroupAccess(workspaceId, groupId, { enabled }) + const updateAccess = useUpdateCredentialGroupAccess() + const setSettingsNavigationBlocked = useSettingsDirtyStore((state) => state.setNavigationBlocked) + const [draft, setDraft] = useState(null) + + if (draft && draft.groupId !== groupId) { + throw new Error('Credential Group access draft cannot move between resources') + } + + const persistedAllowedWorkflowIds = access.data + ? normalizeAllowedWorkflowIds(access.data.allowedWorkflowIds) + : null + const persistedValue = persistedAllowedWorkflowIds + ? serializeAllowedWorkflowIds(persistedAllowedWorkflowIds) + : '' + const allowedWorkflowIds = draft?.allowedWorkflowIds ?? persistedAllowedWorkflowIds + const revision = draft?.expectedRevision ?? access.data?.revision ?? null + const dirty = draft !== null + const workflowIds = new Set(access.data?.workflows.map((workflow) => workflow.id) ?? []) + const selectionsAvailable = Boolean( + allowedWorkflowIds?.every((workflowId) => workflowIds.has(workflowId)) + ) + + const setAllowedWorkflowIds = (nextWorkflowIds: readonly string[], expectedRevision: number) => { + if (!access.data) throw new Error('Credential Group workflow access is unavailable') + const currentRevision = draft?.expectedRevision ?? access.data.revision + if (expectedRevision !== currentRevision) { + throw new Error('Credential Group workflow access changed while it was being edited') + } + const normalizedWorkflowIds = normalizeAllowedWorkflowIds(nextWorkflowIds) + const nextValue = serializeAllowedWorkflowIds(normalizedWorkflowIds) + updateAccess.reset() + setDraft((current) => { + const baseline = current?.baseline ?? persistedValue + if (nextValue === baseline) return null + return { + allowedWorkflowIds: normalizedWorkflowIds, + baseline, + expectedRevision: current?.expectedRevision ?? access.data.revision, + groupId, + } + }) + } + + const discard = () => { + setDraft(null) + updateAccess.reset() + } + + const save = async () => { + if (!draft) return + setSettingsNavigationBlocked(true) + try { + const availableWorkflowIds = new Set(access.data?.workflows.map((workflow) => workflow.id)) + if (draft.allowedWorkflowIds.some((workflowId) => !availableWorkflowIds.has(workflowId))) { + throw new Error('Remove unavailable workflows before saving access') + } + await updateAccess.mutateAsync({ + workspaceId, + groupId, + body: { + expectedRevision: draft.expectedRevision, + allowedWorkflowIds: draft.allowedWorkflowIds, + }, + }) + setDraft(null) + toast.success('Workflow access saved') + } catch (error) { + toast.error(getErrorMessage(error, 'Could not update workflow access')) + } finally { + setSettingsNavigationBlocked(false) + } + } + + return { + allowedWorkflowIds, + revision, + workflows: access.data?.workflows ?? null, + setAllowedWorkflowIds, + discard, + save, + dirty, + error: updateAccess.error + ? getErrorMessage(updateAccess.error, 'Could not update workflow access') + : null, + isPending: access.isPending && !access.data, + loadError: access.data ? null : access.error, + isReady: Boolean(access.data && selectionsAvailable), + saving: updateAccess.isPending, + } +} + +interface CredentialGroupAccessProps { + allowedWorkflowIds: readonly string[] | null + revision: number | null + workflows: CredentialGroupAccessResponse['workflows'] | null + onAllowedWorkflowIdsChange: (workflowIds: readonly string[], expectedRevision: number) => void + error: string | null + isPending: boolean + loadError: unknown + saving: boolean +} + +export function CredentialGroupAccess({ + allowedWorkflowIds, + revision, + workflows, + onAllowedWorkflowIdsChange, + error, + isPending, + loadError, + saving, +}: CredentialGroupAccessProps) { + const [showAddWorkflow, setShowAddWorkflow] = useState(false) + + if (loadError) { + return ( + + {getErrorMessage(loadError, "Couldn't load workflow access")} + + ) + } + if (isPending) return null + if (!workflows) throw new Error('Credential Group workflow catalog is unavailable') + if (!allowedWorkflowIds) throw new Error('Credential Group workflow access is unavailable') + if (revision === null) throw new Error('Credential Group access revision is unavailable') + + const allowedWorkflowIdSet = new Set(allowedWorkflowIds) + if (allowedWorkflowIdSet.size !== allowedWorkflowIds.length) { + throw new Error('Credential Group workflow access contains duplicate workflows') + } + const workflowsById = new Map(workflows.map((workflow) => [workflow.id, workflow])) + for (const workflowId of allowedWorkflowIds) { + if (!workflowsById.has(workflowId)) { + throw new Error( + `Credential Group workflow access references unavailable workflow ${workflowId}` + ) + } + } + const allowedWorkflows = workflows.filter((workflow) => allowedWorkflowIdSet.has(workflow.id)) + const availableWorkflows = workflows.filter((workflow) => !allowedWorkflowIdSet.has(workflow.id)) + + const addWorkflow = (workflowId: string) => { + if (!workflowsById.has(workflowId)) throw new Error(`Workflow ${workflowId} is unavailable`) + if (allowedWorkflowIdSet.has(workflowId)) { + throw new Error(`Workflow ${workflowId} already has Credential Group access`) + } + onAllowedWorkflowIdsChange([...allowedWorkflowIds, workflowId], revision) + } + + const removeWorkflow = (workflowId: string) => { + if (!allowedWorkflowIdSet.has(workflowId)) { + throw new Error(`Workflow ${workflowId} does not have Credential Group access`) + } + onAllowedWorkflowIdsChange( + allowedWorkflowIds.filter((allowedWorkflowId) => allowedWorkflowId !== workflowId), + revision + ) + } + + const sectionAction = ( + setShowAddWorkflow(true)} + disabled={ + saving || + availableWorkflows.length === 0 || + allowedWorkflowIds.length >= CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT + } + > + Add workflow + + ) + + return ( + <> + + {error && ( +

+ {error} +

+ )} + + {allowedWorkflows.length === 0 ? ( + No workflows have access + ) : ( +
+ {allowedWorkflows.map((workflow) => ( + } + iconFilled + title={workflow.name} + description='Deployed runs can use every credential in this group' + disabled={saving} + trailing={ + saving ? undefined : ( + removeWorkflow(workflow.id), + }, + ]} + /> + ) + } + /> + ))} +
+ )} +
+ + {showAddWorkflow && ( + setShowAddWorkflow(false)} + /> + )} + + ) +} diff --git a/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.test.tsx b/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.test.tsx new file mode 100644 index 00000000000..82b186c2112 --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.test.tsx @@ -0,0 +1,96 @@ +/** + * @vitest-environment jsdom + */ + +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, expect, it, vi } from 'vitest' + +vi.mock('@sim/emcn', () => ({ + ChipModal: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalBody: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalField: ({ + children, + }: { + children: ReactNode | ((aria: { 'aria-required'?: boolean }) => ReactNode) + }) => ( +
{typeof children === 'function' ? children({ 'aria-required': true }) : children}
+ ), + ChipModalHeader: ({ children }: { children: ReactNode }) =>

{children}

, + ChipModalFooter: ({ + onCancel, + primaryAction, + }: { + onCancel: () => void + primaryAction: { label: string; onClick: () => void; disabled?: boolean } + }) => ( +
+ + +
+ ), + ChipSelect: ({ + options, + onChange, + }: { + options: Array<{ value: string; label: string }> + onChange: (value: string) => void + }) => ( +
+ {options.map((option) => ( + + ))} +
+ ), +})) + +import { CredentialGroupAddWorkflowModal } from '@/ee/credential-groups/components/credential-group-add-workflow-modal' + +const mountedRoots: Root[] = [] + +afterEach(() => { + act(() => { + for (const root of mountedRoots.splice(0)) root.unmount() + }) +}) + +it('requires one workflow and returns the canonical selected ID', () => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + const onAdd = vi.fn() + const onClose = vi.fn() + act(() => + root.render( + + ) + ) + const button = (label: string) => { + const match = [...container.querySelectorAll('button')].find( + (candidate) => candidate.textContent === label + ) + if (!(match instanceof HTMLButtonElement)) throw new Error(`Button ${label} not found`) + return match + } + + expect(button('Add workflow').disabled).toBe(true) + act(() => button('Finance workflow').click()) + expect(button('Add workflow').disabled).toBe(false) + act(() => button('Add workflow').click()) + + expect(onAdd).toHaveBeenCalledWith('workflow-1') + expect(onClose).toHaveBeenCalledOnce() +}) diff --git a/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.tsx b/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.tsx new file mode 100644 index 00000000000..4dfbeba31cc --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-add-workflow-modal.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipSelect, +} from '@sim/emcn' +import type { CredentialGroupAccessResponse } from '@/lib/api/contracts/credential-groups' + +type CredentialGroupWorkflow = CredentialGroupAccessResponse['workflows'][number] + +interface CredentialGroupAddWorkflowModalProps { + workflows: readonly CredentialGroupWorkflow[] + disabled: boolean + onAdd: (workflowId: string) => void + onClose: () => void +} + +export function CredentialGroupAddWorkflowModal({ + workflows, + disabled, + onAdd, + onClose, +}: CredentialGroupAddWorkflowModalProps) { + const [selectedWorkflowId, setSelectedWorkflowId] = useState('') + + const handleAdd = () => { + if (!selectedWorkflowId) throw new Error('Select a workflow before granting access') + if (!workflows.some((workflow) => workflow.id === selectedWorkflowId)) { + throw new Error(`Workflow ${selectedWorkflowId} is unavailable`) + } + onAdd(selectedWorkflowId) + onClose() + } + + return ( + !open && onClose()} srTitle='Add workflow' size='sm'> + Add workflow + + + {(aria) => ( + ({ + value: workflow.id, + label: workflow.name, + }))} + value={selectedWorkflowId} + onChange={setSelectedWorkflowId} + placeholder='Select workflow' + searchPlaceholder='Search workflows' + searchable + aria-label='Workflow' + disabled={disabled} + fullWidth + dropdownWidth='trigger' + align='start' + {...aria} + /> + )} + + + + + ) +} diff --git a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx index 062b8783cd8..08947b0812b 100644 --- a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx +++ b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx @@ -28,6 +28,10 @@ import { } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' +import { + CredentialGroupAccess, + useCredentialGroupAccessEditor, +} from '@/ee/credential-groups/components/credential-group-access' import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details' import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal' import { @@ -45,11 +49,12 @@ interface CredentialGroupDetailProps { onBack: () => void } -type CredentialGroupTab = 'details' | 'people' +type CredentialGroupTab = 'details' | 'people' | 'access' const CREDENTIAL_GROUP_TABS = [ { value: 'details', label: 'Details' }, { value: 'people', label: 'People' }, + { value: 'access', label: 'Access' }, ] as const interface EnrollmentConnectionsProps { @@ -101,6 +106,11 @@ export function CredentialGroupDetail({ ...credentialGroupTabParam.parser, ...credentialGroupTabUrlKeys, }) + const accessEditor = useCredentialGroupAccessEditor({ + workspaceId, + groupId, + enabled: activeTab === 'access', + }) const [showInvite, setShowInvite] = useState(false) const [showDelete, setShowDelete] = useState(false) const [deletingEnrollmentId, setDeletingEnrollmentId] = useState(null) @@ -128,13 +138,28 @@ export function CredentialGroupDetail({ (name.trim() !== credentialGroup.name || normalizedDescription !== credentialGroup.description) ) - const guard = useSettingsUnsavedGuard({ isDirty: detailsDirty }) + const guard = useSettingsUnsavedGuard({ + isDirty: detailsDirty || accessEditor.dirty, + navigationBlocked: updateGroup.isPending || accessEditor.saving, + }) + const credentialGroupMutationPending = + updateGroup.isPending || accessEditor.saving || resend.isPending || deleteEnrollment.isPending const discardDetails = () => { setDraftName(null) setDraftDescription(null) } + const handleTabChange = (value: string) => { + const nextTab = value as CredentialGroupTab + if (nextTab === activeTab) return + guard.guardBack(() => { + discardDetails() + accessEditor.discard() + void setActiveTab(nextTab) + }) + } + const handleSaveDetails = async () => { if (!credentialGroup || !name.trim()) return try { @@ -150,10 +175,6 @@ export function CredentialGroupDetail({ } } - /** - * Each tab owns its own primary action: Details commits the edited name and - * description, People invites more users. Delete is available from both. - */ const actions: SettingsAction[] = credentialGroup ? [ ...(activeTab === 'details' @@ -165,20 +186,32 @@ export function CredentialGroupDetail({ saveDisabled: !name.trim(), saveTooltip: name.trim() ? undefined : 'Name is required', }) - : [ - { - text: 'Invite users', - icon: Plus, - variant: 'primary' as const, - onSelect: () => setShowInvite(true), - disabled: credentialGroup.status !== 'active' || !configurationReady, - }, - ]), + : activeTab === 'people' + ? [ + { + text: 'Invite users', + icon: Plus, + variant: 'primary' as const, + onSelect: () => setShowInvite(true), + disabled: credentialGroup.status !== 'active' || !configurationReady, + }, + ] + : saveDiscardActions({ + dirty: accessEditor.dirty, + saving: accessEditor.saving, + onSave: () => void accessEditor.save(), + onDiscard: accessEditor.discard, + saveDisabled: !accessEditor.isReady, + saveTooltip: !accessEditor.isReady ? 'Workflow access is unavailable' : undefined, + })), { id: 'delete', text: deleteGroup.isPending ? 'Deleting...' : 'Delete', onSelect: () => setShowDelete(true), - disabled: deleteGroup.isPending, + disabled: deleteGroup.isPending || credentialGroupMutationPending, + tooltip: credentialGroupMutationPending + ? 'Wait for the current Credential Group change to finish' + : undefined, }, ] : [] @@ -239,7 +272,7 @@ export function CredentialGroupDetail({ void setActiveTab(value as CredentialGroupTab)} + onChange={handleTabChange} aria-label='Credential group sections' /> @@ -306,6 +339,20 @@ export function CredentialGroupDetail({ )} )} + + {activeTab === 'access' && ( + + )} )} diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 6f348655939..010ae1250fc 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -408,7 +408,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -600,7 +606,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -637,6 +649,11 @@ describe('WorkflowBlockHandler', () => { expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ workflowId: 'source-workflow-id', executionId: loggingSessionArgs[0][1], + currentWorkflow: { + workflowId: 'source-workflow-id', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, principal: { kind: 'system', serviceId: 'internal', @@ -700,6 +717,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -798,6 +816,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1203,7 +1222,15 @@ describe('WorkflowBlockHandler', () => { ok: true, json: () => Promise.resolve({ - data: { deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} } }, + data: { + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, + }, }), } } @@ -1533,6 +1560,7 @@ describe('WorkflowBlockHandler', () => { expect(mockSafeStart).toHaveBeenCalledTimes(1) const params = mockSafeStart.mock.calls[0][0] expect(params.workspaceId).toBe('workspace-source') + expect(params.deploymentVersionId).toBe('deployment-version-1') expect(params.actorUserId).toBe('owner-9') expect(params.billingAttribution).toEqual({ actorUserId: 'owner-9', @@ -1617,6 +1645,11 @@ describe('WorkflowBlockHandler', () => { expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ workflowId: 'source-workflow-id', executionId: executorOptions[0].contextExtensions.executionId, + currentWorkflow: { + workflowId: 'source-workflow-id', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, principal: { kind: 'system', serviceId: 'internal', @@ -1943,6 +1976,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1996,14 +2030,18 @@ describe('WorkflowBlockHandler', () => { expect(extensions.executionId).toBe('parent-execution-id') expect(extensions.resolvedSecretTraceRegistry).toBe(registry) expect(extensions.executorDelegationOrigin).toEqual({ + subjectUserId: 'user-1', + workflowId: 'parent-workflow-id', + executionId: 'parent-execution-id', + currentWorkflow: { workflowId: 'child-workflow-id', mode: 'draft' }, + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + }) + expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith({ subjectUserId: 'user-1', workflowId: 'parent-workflow-id', executionId: 'parent-execution-id', principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, }) - expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith( - extensions.executorDelegationOrigin - ) expect(extensions.onStream).toBe(ctx.onStream) expect(extensions.childWorkflowContext).toBeDefined() }) @@ -2035,9 +2073,10 @@ describe('WorkflowBlockHandler', () => { await handler.execute(ctx, mockBlock, { workflowId: 'grandchild-workflow-id' }) expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith(ctx.executorDelegationOrigin) - expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toBe( - ctx.executorDelegationOrigin - ) + expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ + ...ctx.executorDelegationOrigin, + currentWorkflow: { workflowId: 'grandchild-workflow-id', mode: 'draft' }, + }) }) }) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 91749fd387d..cacfdc06aba 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -376,6 +376,27 @@ export class WorkflowBlockHandler implements BlockHandler { throw new Error(`Child workflow ${workflowId} not found`) } + if (useDeployed && !childWorkflow.deploymentVersionId) { + throw new Error(`Deployed child workflow ${workflowId} has no deployment version`) + } + + const childWorkflowAuthority = useDeployed + ? { + workflowId, + mode: 'deployment' as const, + deploymentVersionId: childWorkflow.deploymentVersionId as string, + } + : { workflowId, mode: 'draft' as const } + if (!isCustomBlock) { + if (!childExecutorDelegationOrigin) { + throw new Error('Child workflow execution is missing its delegation origin') + } + childExecutorDelegationOrigin = { + ...childExecutorDelegationOrigin, + currentWorkflow: childWorkflowAuthority, + } + } + // Custom blocks are org-scoped and deliberately cross-workspace: the source // workflow lives in the publisher's workspace, not the consumer's. Their // boundary is the org overlay + `getCustomBlockAuthority`, so the @@ -569,6 +590,10 @@ export class WorkflowBlockHandler implements BlockHandler { actorUserId: childUserId, billingAttribution: childBillingAttribution, workspaceId: sourceWorkspaceId, + deploymentVersionId: + childWorkflowAuthority.mode === 'deployment' + ? childWorkflowAuthority.deploymentVersionId + : undefined, variables: childEnvVariablesForLogging, workflowState: childWorkflow.workflowState, ...(correlation ? { triggerData: { correlation } } : {}), @@ -586,6 +611,7 @@ export class WorkflowBlockHandler implements BlockHandler { workspaceId: sourceWorkspaceId, workflowId, }, + currentWorkflow: childWorkflowAuthority, } // The child no longer shares the parent's execution id, so it no longer // hears the parent's cancellation event — bridge it explicitly. @@ -1193,6 +1219,7 @@ export class WorkflowBlockHandler implements BlockHandler { return { name: workflowData.name, workspaceId: (workflowData.workspaceId ?? null) as string | null, + deploymentVersionId: undefined, serializedState: serializedWorkflow, variables: workflowVariables, workflowState: workflowStateWithVariables, @@ -1281,6 +1308,7 @@ export class WorkflowBlockHandler implements BlockHandler { return { name: childName, workspaceId: (wfData?.workspaceId ?? null) as string | null, + deploymentVersionId: deployedState.deploymentVersionId as string | undefined, serializedState: serializedWorkflow, variables: workflowVariables, workflowState: workflowStateWithVariables, diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index 5901184122e..649398ed616 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -1,4 +1,4 @@ -import type { WorkflowExecutionPrincipal } from '@sim/auth/principal' +import type { WorkflowExecutionAuthority, WorkflowExecutionPrincipal } from '@sim/auth/principal' import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution' import type { TraceSpan } from '@/lib/logs/types' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' @@ -368,6 +368,7 @@ export interface ExecutorDelegationOrigin { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } export interface ExecutionContext { diff --git a/apps/sim/hooks/queries/credential-groups.test.tsx b/apps/sim/hooks/queries/credential-groups.test.tsx new file mode 100644 index 00000000000..f1e67ca8db3 --- /dev/null +++ b/apps/sim/hooks/queries/credential-groups.test.tsx @@ -0,0 +1,107 @@ +/** + * @vitest-environment jsdom + */ + +import { act } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CredentialGroupAccessResponse } from '@/lib/api/contracts/credential-groups' + +const mocks = vi.hoisted(() => ({ + requestJson: vi.fn(), +})) + +vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.requestJson })) + +import { useUpdateCredentialGroupAccess } from '@/hooks/queries/credential-groups' +import { credentialGroupKeys } from '@/hooks/queries/utils/credential-group-queries' + +const WORKSPACE_ID = 'workspace-1' +const GROUP_ID = 'group-1' +const ACCESS_QUERY_KEY = credentialGroupKeys.access(WORKSPACE_ID, GROUP_ID) +const CACHED_ACCESS: CredentialGroupAccessResponse = { + revision: 3, + allowedWorkflowIds: ['workflow-1'], + workflows: [ + { id: 'workflow-1', name: 'Finance workflow' }, + { id: 'workflow-2', name: 'Support workflow' }, + ], +} + +const mountedRoots: Root[] = [] + +function renderMutation(queryClient: QueryClient) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + let result: ReturnType | undefined + + function Probe() { + result = useUpdateCredentialGroupAccess() + return null + } + + act(() => + root.render( + + + + ) + ) + + return () => { + if (!result) throw new Error('Credential Group access mutation did not render') + return result + } +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.requestJson.mockResolvedValue({ revision: 4, allowedWorkflowIds: ['workflow-2'] }) +}) + +afterEach(() => { + act(() => { + for (const root of mountedRoots.splice(0)) root.unmount() + }) +}) + +describe('useUpdateCredentialGroupAccess', () => { + it('seeds the exact access cache from the mutation response while preserving the catalog', async () => { + const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } }) + queryClient.setQueryData(ACCESS_QUERY_KEY, CACHED_ACCESS) + const getMutation = renderMutation(queryClient) + + await act(async () => + getMutation().mutateAsync({ + workspaceId: WORKSPACE_ID, + groupId: GROUP_ID, + body: { expectedRevision: 3, allowedWorkflowIds: ['workflow-2'] }, + }) + ) + + expect(queryClient.getQueryData(ACCESS_QUERY_KEY)).toEqual({ + revision: 4, + allowedWorkflowIds: ['workflow-2'], + workflows: CACHED_ACCESS.workflows, + }) + }) + + it('fails before the request when the access cache has not been loaded', async () => { + const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } }) + const getMutation = renderMutation(queryClient) + + await expect( + act(async () => + getMutation().mutateAsync({ + workspaceId: WORKSPACE_ID, + groupId: GROUP_ID, + body: { expectedRevision: 3, allowedWorkflowIds: ['workflow-2'] }, + }) + ) + ).rejects.toThrow('Credential Group access must be loaded before it can be updated') + expect(mocks.requestJson).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/hooks/queries/credential-groups.ts b/apps/sim/hooks/queries/credential-groups.ts index 6aa7af64f29..f8486694e96 100644 --- a/apps/sim/hooks/queries/credential-groups.ts +++ b/apps/sim/hooks/queries/credential-groups.ts @@ -4,17 +4,21 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tansta import { requestJson } from '@/lib/api/client/request' import type { ContractBodyInput } from '@/lib/api/contracts' import { + type CredentialGroupAccessResponse, createCredentialGroupContract, deleteCredentialGroupContract, deleteCredentialGroupEnrollmentContract, + getCredentialGroupAccessContract, getCredentialGroupContract, inviteCredentialGroupEnrollmentsContract, resendCredentialGroupEnrollmentContract, startSlackCredentialGroupConfigurationContract, + updateCredentialGroupAccessContract, updateCredentialGroupContract, } from '@/lib/api/contracts/credential-groups' import type { ContractJsonResponse } from '@/lib/api/contracts/types' import { + CREDENTIAL_GROUP_ACCESS_STALE_TIME, CREDENTIAL_GROUP_DETAIL_STALE_TIME, CREDENTIAL_GROUP_LIST_STALE_TIME, credentialGroupKeys, @@ -56,6 +60,72 @@ export function useCredentialGroupDetail(workspaceId?: string, groupId?: string) }) } +interface UseCredentialGroupAccessOptions { + enabled?: boolean +} + +export function useCredentialGroupAccess( + workspaceId?: string, + groupId?: string, + { enabled = true }: UseCredentialGroupAccessOptions = {} +) { + return useQuery({ + queryKey: credentialGroupKeys.access(workspaceId, groupId), + queryFn: ({ signal }) => { + if (!workspaceId || !groupId) { + throw new Error('Credential Group access identifiers are required') + } + return requestJson(getCredentialGroupAccessContract, { + params: { id: workspaceId, groupId }, + signal, + }) + }, + enabled: Boolean(workspaceId && groupId && enabled), + staleTime: CREDENTIAL_GROUP_ACCESS_STALE_TIME, + retryOnMount: true, + }) +} + +export function useUpdateCredentialGroupAccess() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async ({ + workspaceId, + groupId, + body, + }: { + workspaceId: string + groupId: string + body: ContractBodyInput + }) => + requestJson(updateCredentialGroupAccessContract, { + params: { id: workspaceId, groupId }, + body, + }), + onMutate: async (variables) => { + const queryKey = credentialGroupKeys.access(variables.workspaceId, variables.groupId) + await queryClient.cancelQueries({ queryKey, exact: true }) + const cachedAccess = queryClient.getQueryData(queryKey) + if (!cachedAccess) { + throw new Error('Credential Group access must be loaded before it can be updated') + } + return { queryKey, workflows: cachedAccess.workflows } + }, + onSuccess: (access, _variables, context) => { + if (!context) throw new Error('Credential Group access mutation context is unavailable') + queryClient.setQueryData(context.queryKey, { + ...access, + workflows: context.workflows, + }) + }, + onSettled: (_data, _error, variables) => + queryClient.invalidateQueries({ + queryKey: credentialGroupKeys.access(variables.workspaceId, variables.groupId), + exact: true, + }), + }) +} + export function useCreateCredentialGroup() { const queryClient = useQueryClient() return useMutation({ diff --git a/apps/sim/hooks/queries/utils/credential-group-queries.ts b/apps/sim/hooks/queries/utils/credential-group-queries.ts index 780b2f31f49..e7964c04ad2 100644 --- a/apps/sim/hooks/queries/utils/credential-group-queries.ts +++ b/apps/sim/hooks/queries/utils/credential-group-queries.ts @@ -4,6 +4,8 @@ import { listCredentialGroupsContract } from '@/lib/api/contracts/credential-gro export const CREDENTIAL_GROUP_DETAIL_STALE_TIME = Number.POSITIVE_INFINITY export const CREDENTIAL_GROUP_LIST_STALE_TIME = 30 * 1000 +export const CREDENTIAL_GROUP_ACCESS_STALE_TIME = 30 * 1000 +const CREDENTIAL_GROUP_ACCESS_QUERY_VERSION = 4 export const credentialGroupKeys = { all: ['credential-groups'] as const, @@ -12,6 +14,12 @@ export const credentialGroupKeys = { details: () => [...credentialGroupKeys.all, 'detail'] as const, detail: (workspaceId?: string, groupId?: string) => [...credentialGroupKeys.details(), workspaceId ?? '', groupId ?? ''] as const, + access: (workspaceId?: string, groupId?: string) => + [ + ...credentialGroupKeys.detail(workspaceId, groupId), + 'access', + CREDENTIAL_GROUP_ACCESS_QUERY_VERSION, + ] as const, } export async function fetchCredentialGroupList( diff --git a/apps/sim/lib/api/contracts/credential-groups.test.ts b/apps/sim/lib/api/contracts/credential-groups.test.ts index aee60aa2d7b..5f7edf584e3 100644 --- a/apps/sim/lib/api/contracts/credential-groups.test.ts +++ b/apps/sim/lib/api/contracts/credential-groups.test.ts @@ -1,13 +1,20 @@ import { describe, expect, it } from 'vitest' import { createCredentialGroupBodySchema, + credentialGroupAccessPolicySchema, + credentialGroupAccessResponseSchema, credentialGroupEnrollmentDetailSchema, credentialGroupEnrollmentListQuerySchema, credentialGroupSchema, inviteCredentialGroupEnrollmentsBodySchema, sharedCredentialGroupOAuthCallbackContract, + updateCredentialGroupAccessBodySchema, updateCredentialGroupBodySchema, } from '@/lib/api/contracts/credential-groups' +import { + CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT, + CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT, +} from '@/lib/credential-groups/workflow-access-limits' describe('credential group contracts', () => { it('describes the shared managed OAuth callback as a redirect', () => { @@ -201,4 +208,84 @@ describe('credential group contracts', () => { expect(result.connections).toEqual([{ provider: 'gmail', status: 'active', count: 2 }]) }) + + it('accepts a bounded unique workflow access selection', () => { + const result = updateCredentialGroupAccessBodySchema.parse({ + expectedRevision: 3, + allowedWorkflowIds: ['workflow-1', 'workflow-2'], + }) + + expect(result.allowedWorkflowIds).toEqual(['workflow-1', 'workflow-2']) + }) + + it('requires the bounded workflow catalog only on access reads', () => { + const access = { revision: 1, allowedWorkflowIds: ['workflow-1'] } + + expect( + credentialGroupAccessResponseSchema.parse({ + ...access, + workflows: [{ id: 'workflow-1', name: 'Support workflow' }], + }).workflows + ).toEqual([{ id: 'workflow-1', name: 'Support workflow' }]) + expect(credentialGroupAccessResponseSchema.safeParse(access).success).toBe(false) + expect( + credentialGroupAccessResponseSchema.safeParse({ + ...access, + workflows: Array.from( + { length: CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT + 1 }, + (_, index) => ({ id: `workflow-${index}`, name: `Workflow ${index}` }) + ), + }).success + ).toBe(false) + expect(credentialGroupAccessPolicySchema.safeParse(access).success).toBe(true) + expect( + credentialGroupAccessPolicySchema.safeParse({ + ...access, + workflows: [], + }).success + ).toBe(false) + }) + + it('rejects revision zero, duplicate workflows, oversized selections, and policy documents', () => { + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 0, + allowedWorkflowIds: [], + }).success + ).toBe(false) + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 1, + allowedWorkflowIds: ['workflow-1', 'workflow-1'], + }).success + ).toBe(false) + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 1, + allowedWorkflowIds: [' workflow-1'], + }).success + ).toBe(false) + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 1, + allowedWorkflowIds: [' '], + }).success + ).toBe(false) + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 1, + allowedWorkflowIds: Array.from( + { length: CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT + 1 }, + (_, index) => `workflow-${index}` + ), + }).success + ).toBe(false) + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 1, + allowedWorkflowIds: [], + document: { version: 1, resource: { type: 'credential_group', id: 'group-1' } }, + }).success + ).toBe(false) + }) }) diff --git a/apps/sim/lib/api/contracts/credential-groups.ts b/apps/sim/lib/api/contracts/credential-groups.ts index 7bffa8d050a..770f6f2b65a 100644 --- a/apps/sim/lib/api/contracts/credential-groups.ts +++ b/apps/sim/lib/api/contracts/credential-groups.ts @@ -1,10 +1,15 @@ import { z } from 'zod' -import { workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { workflowIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives' import { defineRouteContract } from '@/lib/api/contracts/types' import { CREDENTIAL_GROUP_PROVIDER_IDS, CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS, } from '@/lib/credential-groups/providers' +import { + CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT, + CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT, + CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH, +} from '@/lib/credential-groups/workflow-access-limits' export const credentialGroupProviderSchema = z.enum(CREDENTIAL_GROUP_PROVIDER_IDS) export const credentialGroupStatusSchema = z.enum(['active', 'disabled']) @@ -115,6 +120,61 @@ export type CredentialGroupEnrollmentConnection = z.output< > export type CredentialGroupEnrollmentDetail = z.output +export const credentialGroupAccessPolicySchema = z + .object({ + revision: z.number().int().positive(), + allowedWorkflowIds: z + .array( + workflowIdSchema + .max(128, 'Workflow ID is too long') + .refine((workflowId) => workflowId === workflowId.trim(), { + message: 'Workflow ID must not have surrounding whitespace', + }) + ) + .max( + CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT, + `Select at most ${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} workflows` + ) + .superRefine((workflowIds, ctx) => { + const seen = new Set() + for (const [index, workflowId] of workflowIds.entries()) { + if (seen.has(workflowId)) { + ctx.addIssue({ + code: 'custom', + path: [index], + message: 'Workflow selections must be unique', + }) + } + seen.add(workflowId) + } + }), + }) + .strict() + +export type CredentialGroupAccessPolicy = z.output + +export const resourcePolicyWorkflowSchema = z + .object({ + id: z.string().min(1).max(128), + name: z.string().max(CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH), + }) + .strict() + +export const credentialGroupAccessResponseSchema = credentialGroupAccessPolicySchema.extend({ + workflows: z.array(resourcePolicyWorkflowSchema).max(CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT), +}) + +export type CredentialGroupAccessResponse = z.output + +export const updateCredentialGroupAccessBodySchema = z + .object({ + expectedRevision: z.number().int().positive(), + allowedWorkflowIds: credentialGroupAccessPolicySchema.shape.allowedWorkflowIds, + }) + .strict() + +export type UpdateCredentialGroupAccessBody = z.input + export const credentialGroupWorkspaceParamsSchema = z.object({ id: workspaceIdSchema, }) @@ -390,6 +450,21 @@ export const updateCredentialGroupContract = defineRouteContract({ }, }) +export const getCredentialGroupAccessContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/credential-groups/[groupId]/access', + params: credentialGroupDetailParamsSchema, + response: { mode: 'json', schema: credentialGroupAccessResponseSchema }, +}) + +export const updateCredentialGroupAccessContract = defineRouteContract({ + method: 'PUT', + path: '/api/workspaces/[id]/credential-groups/[groupId]/access', + params: credentialGroupDetailParamsSchema, + body: updateCredentialGroupAccessBodySchema, + response: { mode: 'json', schema: credentialGroupAccessPolicySchema }, +}) + export const startSlackCredentialGroupConfigurationContract = defineRouteContract({ method: 'POST', path: '/api/workspaces/[id]/credential-groups/[groupId]/slack-managed-users', diff --git a/apps/sim/lib/auth/internal-delegation.test.ts b/apps/sim/lib/auth/internal-delegation.test.ts index a041ec712a8..f7bd611db2a 100644 --- a/apps/sim/lib/auth/internal-delegation.test.ts +++ b/apps/sim/lib/auth/internal-delegation.test.ts @@ -3,14 +3,19 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockResolveWorkflow, mockResolveRun } = vi.hoisted(() => ({ - mockResolveWorkflow: vi.fn(), - mockResolveRun: vi.fn(), -})) +const { mockResolveWorkflow, mockResolveRun, mockResolveExecution, mockResolveDeploymentVersion } = + vi.hoisted(() => ({ + mockResolveWorkflow: vi.fn(), + mockResolveRun: vi.fn(), + mockResolveExecution: vi.fn(), + mockResolveDeploymentVersion: vi.fn(), + })) vi.mock('@/lib/workflows/application/context', () => ({ resolveActiveWorkflowApplicationContext: mockResolveWorkflow, resolveActiveWorkflowRunApplicationContext: mockResolveRun, + resolveActiveWorkflowExecutionApplicationContext: mockResolveExecution, + resolveActiveWorkflowDeploymentVersionApplicationContext: mockResolveDeploymentVersion, })) import { @@ -40,6 +45,17 @@ describe('bindInternalExecutorDelegation', () => { workspaceId: 'workspace-1', runId: 'execution-1', }) + mockResolveExecution.mockResolvedValue({ + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + runId: 'execution-1', + deploymentVersionId: 'deployment-version-1', + }) + mockResolveDeploymentVersion.mockResolvedValue({ + workflowId: 'child-workflow', + workspaceId: 'workspace-1', + deploymentVersionId: 'deployment-version-1', + }) }) it('derives workspace authority from the canonical workflow', async () => { @@ -86,6 +102,157 @@ describe('bindInternalExecutorDelegation', () => { }) }) + it('binds deployed child authority to its exact historical deployment version', async () => { + const currentWorkflow = { + workflowId: 'child-workflow', + mode: 'deployment' as const, + deploymentVersionId: 'deployment-version-1', + } + + const principal = await bindInternalExecutorDelegation( + { ...claims, executionId: 'execution-1', currentWorkflow }, + { audience: 'sim:credential-groups' } + ) + + expect(mockResolveExecution).toHaveBeenCalledWith({ + runId: 'execution-1', + assertedWorkflowId: 'workflow-1', + }) + expect(mockResolveDeploymentVersion).toHaveBeenCalledWith({ + workflowId: 'child-workflow', + deploymentVersionId: 'deployment-version-1', + assertedWorkspaceId: 'workspace-1', + }) + expect(principal.delegationContext.currentWorkflow).toEqual(currentWorkflow) + }) + + it('rejects a deployed child version that does not belong to the claimed workflow', async () => { + mockResolveDeploymentVersion.mockRejectedValue( + new OrchestrationError('not_found', 'Workflow deployment version not found') + ) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + executionId: 'execution-1', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + }) + + it('rejects current workflow authority from another workspace', async () => { + mockResolveWorkflow.mockResolvedValueOnce({ + workflowId: 'child-workflow', + workspaceId: 'workspace-2', + }) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + executionId: 'execution-1', + currentWorkflow: { workflowId: 'child-workflow', mode: 'draft' }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + expect(mockResolveDeploymentVersion).not.toHaveBeenCalled() + }) + + it('does not disguise current-workflow infrastructure failures as invalid credentials', async () => { + const infrastructureError = new Error('deployment database unavailable') + mockResolveDeploymentVersion.mockRejectedValue(infrastructureError) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + executionId: 'execution-1', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBe(infrastructureError) + }) + + it('rejects current workflow authority without a canonical execution binding', async () => { + await expect( + bindInternalExecutorDelegation( + { + ...claims, + currentWorkflow: { workflowId: 'workflow-1', mode: 'draft' }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + + expect(mockResolveExecution).not.toHaveBeenCalled() + }) + + it('binds root deployment authority to the immutable version recorded on the run', async () => { + const currentWorkflow = { + workflowId: 'workflow-1', + mode: 'deployment' as const, + deploymentVersionId: 'deployment-version-1', + } + + const principal = await bindInternalExecutorDelegation( + { ...claims, executionId: 'execution-1', currentWorkflow }, + { audience: 'sim:credential-groups' } + ) + + expect(principal.delegationContext.currentWorkflow).toEqual(currentWorkflow) + expect(mockResolveDeploymentVersion).not.toHaveBeenCalled() + }) + + it('rejects root deployment authority that disagrees with the durable run version', async () => { + mockResolveExecution.mockResolvedValueOnce({ + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + runId: 'execution-1', + deploymentVersionId: 'deployment-version-new', + }) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + executionId: 'execution-1', + currentWorkflow: { + workflowId: 'workflow-1', + mode: 'deployment', + deploymentVersionId: 'deployment-version-old', + }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + }) + + it('rejects draft root authority for a durably deployed run', async () => { + await expect( + bindInternalExecutorDelegation( + { + ...claims, + executionId: 'execution-1', + currentWorkflow: { workflowId: 'workflow-1', mode: 'draft' }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + }) + it('fails before canonical loading when the domain audience is missing', async () => { await expect(bindInternalExecutorDelegation(claims, { audience: ' ' })).rejects.toThrow( 'Internal delegation audience must not be empty' diff --git a/apps/sim/lib/auth/internal-delegation.ts b/apps/sim/lib/auth/internal-delegation.ts index b67ac79f2b4..7f03930ae03 100644 --- a/apps/sim/lib/auth/internal-delegation.ts +++ b/apps/sim/lib/auth/internal-delegation.ts @@ -5,7 +5,10 @@ import type { import type { VerifiedInternalDelegation } from '@/lib/auth/internal' import { asOrchestrationError } from '@/lib/core/orchestration/types' import { + type ActiveWorkflowApplicationContext, resolveActiveWorkflowApplicationContext, + resolveActiveWorkflowDeploymentVersionApplicationContext, + resolveActiveWorkflowExecutionApplicationContext, resolveActiveWorkflowRunApplicationContext, } from '@/lib/workflows/application/context' @@ -28,14 +31,25 @@ export async function bindInternalExecutorDelegation( ): Promise { if (!options.audience.trim()) throw new Error('Internal delegation audience must not be empty') - let context + let context: ActiveWorkflowApplicationContext + let rootDeploymentVersionId: string | null | undefined try { - context = claims.executionId - ? await resolveActiveWorkflowRunApplicationContext({ - runId: claims.executionId, - assertedWorkflowId: claims.workflowId, - }) - : await resolveActiveWorkflowApplicationContext({ workflowId: claims.workflowId }) + if (claims.currentWorkflow) { + if (!claims.executionId) throw new InvalidInternalDelegationBindingError() + const executionContext = await resolveActiveWorkflowExecutionApplicationContext({ + runId: claims.executionId, + assertedWorkflowId: claims.workflowId, + }) + context = executionContext + rootDeploymentVersionId = executionContext.deploymentVersionId + } else if (claims.executionId) { + context = await resolveActiveWorkflowRunApplicationContext({ + runId: claims.executionId, + assertedWorkflowId: claims.workflowId, + }) + } else { + context = await resolveActiveWorkflowApplicationContext({ workflowId: claims.workflowId }) + } } catch (error) { if (asOrchestrationError(error)?.code === 'not_found') { throw new InvalidInternalDelegationBindingError() @@ -43,6 +57,40 @@ export async function bindInternalExecutorDelegation( throw error } + if (claims.currentWorkflow) { + if (claims.currentWorkflow.workflowId === context.workflowId) { + const matchesRootExecution = + claims.currentWorkflow.mode === 'draft' + ? rootDeploymentVersionId === null + : rootDeploymentVersionId === claims.currentWorkflow.deploymentVersionId + if (!matchesRootExecution) { + throw new InvalidInternalDelegationBindingError() + } + } else { + try { + const currentContext = + claims.currentWorkflow.mode === 'deployment' + ? await resolveActiveWorkflowDeploymentVersionApplicationContext({ + workflowId: claims.currentWorkflow.workflowId, + deploymentVersionId: claims.currentWorkflow.deploymentVersionId, + assertedWorkspaceId: context.workspaceId, + }) + : await resolveActiveWorkflowApplicationContext({ + workflowId: claims.currentWorkflow.workflowId, + assertedWorkspaceId: context.workspaceId, + }) + if (currentContext.workspaceId !== context.workspaceId) { + throw new InvalidInternalDelegationBindingError() + } + } catch (error) { + if (asOrchestrationError(error)?.code === 'not_found') { + throw new InvalidInternalDelegationBindingError() + } + throw error + } + } + } + return { kind: 'delegated', serviceId: 'executor', @@ -58,6 +106,7 @@ export async function bindInternalExecutorDelegation( workflowId: context.workflowId, ...(claims.executionId ? { executionId: claims.executionId } : {}), ...(claims.principal ? { principal: claims.principal } : {}), + ...(claims.currentWorkflow ? { currentWorkflow: claims.currentWorkflow } : {}), }, } } diff --git a/apps/sim/lib/auth/internal.test.ts b/apps/sim/lib/auth/internal.test.ts index 9fcf4b7830b..6bf619889b0 100644 --- a/apps/sim/lib/auth/internal.test.ts +++ b/apps/sim/lib/auth/internal.test.ts @@ -135,6 +135,52 @@ describe('internal executor delegation claims', () => { }) }) + it('round-trips the currently executing deployed workflow authority', async () => { + const token = await generateInternalDelegationToken({ + subjectUserId: 'user-1', + workflowId: 'root-workflow', + executionId: 'execution-1', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }) + + await expect(verifyInternalDelegationToken(token)).resolves.toMatchObject({ + workflowId: 'root-workflow', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }) + }) + + it('refuses to issue current workflow authority without an execution binding', async () => { + await expect( + generateInternalDelegationToken({ + subjectUserId: 'user-1', + workflowId: 'root-workflow', + currentWorkflow: { workflowId: 'root-workflow', mode: 'draft' }, + }) + ).rejects.toThrow('Internal delegation currentWorkflow requires executionId') + }) + + it('rejects malformed workflow authority instead of dropping its fields', async () => { + await expect( + generateInternalDelegationToken({ + subjectUserId: 'user-1', + workflowId: 'root-workflow', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'draft', + unexpected: true, + } as never, + }) + ).rejects.toBeInstanceOf(InvalidInternalDelegationTokenError) + }) + it('rejects laundering actorless or external principals into a Sim user subject', async () => { await expect( generateInternalDelegationToken({ diff --git a/apps/sim/lib/auth/internal.ts b/apps/sim/lib/auth/internal.ts index 3707e04348e..fe410edf9a4 100644 --- a/apps/sim/lib/auth/internal.ts +++ b/apps/sim/lib/auth/internal.ts @@ -2,6 +2,7 @@ import { parsePrincipal, resolvePrincipalSubject, serializePrincipal, + type WorkflowExecutionAuthority, type WorkflowExecutionPrincipal, } from '@sim/auth/principal' import { createLogger } from '@sim/logger' @@ -26,6 +27,7 @@ export interface GenerateInternalDelegationTokenInput { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } export interface VerifiedInternalDelegation { @@ -34,6 +36,7 @@ export interface VerifiedInternalDelegation { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority delegationId: string issuedAt: Date expiresAt: Date @@ -99,6 +102,34 @@ function requireNonEmptyDelegationClaim(value: string, name: string): string { return value } +function parseWorkflowExecutionAuthority(value: unknown): WorkflowExecutionAuthority { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new InvalidInternalDelegationTokenError() + } + const authority = value as Record + const workflowId = readVerifiedDelegationClaim(authority.workflowId) + if (!workflowId) throw new InvalidInternalDelegationTokenError() + if (authority.mode === 'draft') { + if (Object.keys(authority).some((key) => !['workflowId', 'mode'].includes(key))) { + throw new InvalidInternalDelegationTokenError() + } + return { workflowId, mode: 'draft' } + } + if (authority.mode === 'deployment') { + const deploymentVersionId = readVerifiedDelegationClaim(authority.deploymentVersionId) + if ( + !deploymentVersionId || + Object.keys(authority).some( + (key) => !['workflowId', 'mode', 'deploymentVersionId'].includes(key) + ) + ) { + throw new InvalidInternalDelegationTokenError() + } + return { workflowId, mode: 'deployment', deploymentVersionId } + } + throw new InvalidInternalDelegationTokenError() +} + /** Generates an executor token bound to its workflow origin and authenticated caller. */ export async function generateInternalDelegationToken( input: GenerateInternalDelegationTokenInput @@ -126,16 +157,23 @@ export async function generateInternalDelegationToken( throw new Error('Internal delegation requires a workflow principal or Sim user subject') } const workflowId = requireNonEmptyDelegationClaim(input.workflowId, 'workflowId') + const currentWorkflow = input.currentWorkflow + ? parseWorkflowExecutionAuthority(input.currentWorkflow) + : undefined const issuedAtSeconds = Math.floor(Date.now() / 1000) const executionId = input.executionId ? requireNonEmptyDelegationClaim(input.executionId, 'executionId') : undefined + if (currentWorkflow && !executionId) { + throw new Error('Internal delegation currentWorkflow requires executionId') + } let token = new SignJWT({ type: 'internal_delegation', serviceId: 'executor', workflowId, ...(input.principal ? { principal: serializePrincipal(input.principal) } : {}), + ...(currentWorkflow ? { currentWorkflow } : {}), ...(executionId ? { executionId } : {}), }) .setProtectedHeader({ alg: 'HS256' }) @@ -177,6 +215,7 @@ export async function verifyInternalDelegationToken( const delegationId = readVerifiedDelegationClaim(payload.jti) const nowSeconds = Math.floor(Date.now() / 1000) let principal: WorkflowExecutionPrincipal | undefined + let currentWorkflow: WorkflowExecutionAuthority | undefined if (payload.principal !== undefined) { try { principal = parsePrincipal(payload.principal) @@ -184,12 +223,16 @@ export async function verifyInternalDelegationToken( throw new InvalidInternalDelegationTokenError() } } + if (payload.currentWorkflow !== undefined) { + currentWorkflow = parseWorkflowExecutionAuthority(payload.currentWorkflow) + } if ( payload.type !== 'internal_delegation' || payload.serviceId !== 'executor' || !workflowId || executionId === null || + (currentWorkflow !== undefined && executionId === undefined) || !delegationId || typeof payload.iat !== 'number' || typeof payload.exp !== 'number' || @@ -215,6 +258,7 @@ export async function verifyInternalDelegationToken( ...(subjectUserId ? { subjectUserId } : {}), workflowId, ...(principal ? { principal } : {}), + ...(currentWorkflow ? { currentWorkflow } : {}), ...(executionId ? { executionId } : {}), delegationId, issuedAt: new Date(payload.iat * 1000), diff --git a/apps/sim/lib/core/application/authorized-workspace-use-case.test.ts b/apps/sim/lib/core/application/authorized-workspace-use-case.test.ts index 067ad94ff10..bd9b0655d11 100644 --- a/apps/sim/lib/core/application/authorized-workspace-use-case.test.ts +++ b/apps/sim/lib/core/application/authorized-workspace-use-case.test.ts @@ -34,6 +34,7 @@ vi.mock('@sim/platform-authz/workspace', () => ({ import { AuditAction, AuditResourceType } from '@sim/audit' import { defineAuthorizedWorkspaceUseCase, defineWorkspaceOperation } from '@/lib/core/application' import type { OrchestrationError } from '@/lib/core/orchestration/types' +import { CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION } from '@/lib/resource-policies/registry' const operation = defineWorkspaceOperation({ id: 'test.rename', @@ -57,6 +58,17 @@ const workspaceKeyOperation = defineWorkspaceOperation({ principalKinds: ['workspace_api_key'], }) +const resourcePolicyOperation = defineWorkspaceOperation({ + id: 'test.resource_policy', + minimumRole: 'write', + workspaceApiKey: 'deny', + principalKinds: ['session'], + resourcePolicy: { + resourceType: 'credential_group', + action: CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION, + }, +}) + interface TestInput { resourceId: string } @@ -191,13 +203,14 @@ describe('defineAuthorizedWorkspaceUseCase', () => { return { ok: true as const } }) const useCase = defineAuthorizedWorkspaceUseCase({ - operation, + operation: resourcePolicyOperation, resolveContext: async (_args: { principal: SessionPrincipal; input: TestInput }) => { mocks.events.push('canonicalLoad') return canonicalContext }, authorizationOptions: {}, - authorizeResource() { + authorizeResource({ resourcePolicy }) { + expect(resourcePolicy).toBe(resourcePolicyOperation.resourcePolicy) mocks.events.push('resourceAuthorization') }, execute, @@ -240,6 +253,45 @@ describe('defineAuthorizedWorkspaceUseCase', () => { ]) }) + it('requires policy-bound operations to define resource authorization', async () => { + const execute = vi.fn(async () => ({ ok: true as const })) + expect(() => + defineAuthorizedWorkspaceUseCase({ + operation: resourcePolicyOperation, + resolveContext: async (_args: { principal: SessionPrincipal; input: TestInput }) => + canonicalContext, + authorizationOptions: {}, + execute, + }) + ).toThrow('Operation test.resource_policy requires resource policy authorization') + + expect(execute).not.toHaveBeenCalled() + }) + + it('keeps non-policy resource authorization available to ordinary operations', async () => { + const authorizeResource = vi.fn() + const useCase = defineAuthorizedWorkspaceUseCase({ + operation, + resolveContext: async (_args: { principal: SessionPrincipal; input: TestInput }) => + canonicalContext, + authorizationOptions: {}, + authorizeResource, + async execute() { + return { ok: true as const } + }, + }) + + await expect( + useCase.execute({ + principal: sessionPrincipal, + input: { resourceId: 'resource-1' }, + }) + ).resolves.toEqual({ ok: true }) + expect(authorizeResource).toHaveBeenCalledWith( + expect.objectContaining({ context: canonicalContext }) + ) + }) + it('supports zero or many semantic audit entries', async () => { const buildUseCase = (auditCount: number) => defineAuthorizedWorkspaceUseCase({ diff --git a/apps/sim/lib/core/application/authorized-workspace-use-case.ts b/apps/sim/lib/core/application/authorized-workspace-use-case.ts index 0d37830b458..6cf9c176bc6 100644 --- a/apps/sim/lib/core/application/authorized-workspace-use-case.ts +++ b/apps/sim/lib/core/application/authorized-workspace-use-case.ts @@ -13,6 +13,7 @@ import type { WorkspaceOperation, } from '@/lib/core/application/workspace-operation' import type { OrchestrationRequestContext } from '@/lib/core/orchestration/types' +import type { ResourcePolicyBinding } from '@/lib/resource-policies/registry' export interface WorkspaceUseCaseAuditEntry { action: AuditActionType @@ -56,8 +57,8 @@ export interface AuthorizedWorkspaceUseCaseDefinition< | (( args: AuthorizedWorkspaceUseCaseContext ) => WorkspaceAuthorizationOptions | Promise>) - /** Applies current domain-resource policy after workspace authorization. */ - authorizeResource?(args: AuthorizedWorkspaceUseCaseContext): void | Promise + /** Receives the operation-owned policy binding when the operation declares one. */ + authorizeResource?(args: AuthorizedWorkspaceResourceUseCaseContext): void | Promise execute(args: AuthorizedWorkspaceUseCaseContext): Promise projectAudit?( args: AuthorizedWorkspaceUseCaseResultContext @@ -65,6 +66,20 @@ export interface AuthorizedWorkspaceUseCaseDefinition< afterSuccess?(args: AuthorizedWorkspaceUseCaseResultContext): void | Promise } +type ResourcePolicyForOperation = O extends { + readonly resourcePolicy: infer Binding extends ResourcePolicyBinding +} + ? Binding + : never + +export type AuthorizedWorkspaceResourceUseCaseContext< + O extends WorkspaceOperation, + I, + C extends WorkspaceAuthorizationContext, +> = AuthorizedWorkspaceUseCaseContext & { + resourcePolicy: ResourcePolicyForOperation +} + function isAuthorizationOptionsResolver< O extends WorkspaceOperation, I, @@ -111,6 +126,28 @@ export function defineAuthorizedWorkspaceUseCase< C extends WorkspaceAuthorizationContext, R, >(definition: AuthorizedWorkspaceUseCaseDefinition): OperationUseCase { + const resourceAuthorization = (() => { + if ('resourcePolicy' in definition.operation && definition.operation.resourcePolicy) { + const authorizeResource = definition.authorizeResource + if (!authorizeResource) { + throw new Error( + `Operation ${definition.operation.id} requires resource policy authorization` + ) + } + const resourcePolicy = definition.operation.resourcePolicy as ResourcePolicyForOperation + return (executionContext: AuthorizedWorkspaceUseCaseContext) => + authorizeResource({ + ...executionContext, + resourcePolicy, + } as AuthorizedWorkspaceResourceUseCaseContext) + } + const authorizeResource = definition.authorizeResource + return authorizeResource + ? (executionContext: AuthorizedWorkspaceUseCaseContext) => + authorizeResource(executionContext as AuthorizedWorkspaceResourceUseCaseContext) + : undefined + })() + /** * Everything that runs before the business transaction: allowed-principal * check, canonical load, asserted-scope comparison, current workspace and @@ -148,7 +185,7 @@ export function defineAuthorizedWorkspaceUseCase< context, authorizationOptions ) - await definition.authorizeResource?.(executionContext) + await resourceAuthorization?.(executionContext) return executionContext } diff --git a/apps/sim/lib/core/application/index.ts b/apps/sim/lib/core/application/index.ts index d3de7b7e599..04d1dddbe31 100644 --- a/apps/sim/lib/core/application/index.ts +++ b/apps/sim/lib/core/application/index.ts @@ -1,5 +1,6 @@ export { principalAuditSource } from '@/lib/core/application/audit-source' export { + type AuthorizedWorkspaceResourceUseCaseContext, type AuthorizedWorkspaceUseCaseContext, type AuthorizedWorkspaceUseCaseDefinition, type AuthorizedWorkspaceUseCaseResultContext, diff --git a/apps/sim/lib/core/application/workspace-operation.test.ts b/apps/sim/lib/core/application/workspace-operation.test.ts index 20e5ed02822..2e99e65eec1 100644 --- a/apps/sim/lib/core/application/workspace-operation.test.ts +++ b/apps/sim/lib/core/application/workspace-operation.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from 'vitest' import { defineWorkspaceOperation } from '@/lib/core/application/workspace-operation' +import { CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION } from '@/lib/resource-policies/registry' describe('defineWorkspaceOperation delegated service policy', () => { it('preserves and freezes an explicit delegated service allowlist', () => { @@ -52,4 +53,40 @@ describe('defineWorkspaceOperation delegated service policy', () => { } as never) ).toThrow('Operation test.duplicate_service_policy declares duplicate delegated services') }) + + it('preserves, validates, and freezes its resource policy binding', () => { + const operation = defineWorkspaceOperation({ + id: 'test.credential_use', + minimumRole: 'read', + workspaceApiKey: 'deny', + principalKinds: ['delegated'], + delegatedServices: ['executor'], + resourcePolicy: { + resourceType: 'credential_group', + action: CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION, + }, + }) + + expect(operation.resourcePolicy).toEqual({ + resourceType: 'credential_group', + action: 'credential_groups.credentials.use', + }) + expect(Object.isFrozen(operation.resourcePolicy)).toBe(true) + }) + + it('fails fast for an action outside the operation resource type', () => { + expect(() => + defineWorkspaceOperation({ + id: 'test.invalid_resource_policy', + minimumRole: 'read', + workspaceApiKey: 'deny', + principalKinds: ['delegated'], + delegatedServices: ['executor'], + resourcePolicy: { + resourceType: 'credential_group', + action: 'credentials.invalid', + }, + } as never) + ).toThrow('Action credentials.invalid does not apply to resource policy type credential_group') + }) }) diff --git a/apps/sim/lib/core/application/workspace-operation.ts b/apps/sim/lib/core/application/workspace-operation.ts index 852a4b6458d..45f35de22d1 100644 --- a/apps/sim/lib/core/application/workspace-operation.ts +++ b/apps/sim/lib/core/application/workspace-operation.ts @@ -1,6 +1,10 @@ import type { DelegatedPrincipal, DelegatedServiceId, Principal } from '@sim/auth/principal' import type { PermissionType } from '@sim/platform-authz/workspace' import type { ApplicationOperation, PrincipalKind } from '@/lib/core/application/operation' +import { + type ResourcePolicyBinding, + requireResourcePolicyBinding, +} from '@/lib/resource-policies/registry' type WorkspaceApiKeyPolicy = R extends 'admin' ? 'deny' : 'allow' | 'deny' @@ -59,17 +63,25 @@ type DelegatedPrincipalConsistency< } : { readonly delegatedServices?: never } +type ResourcePolicyOperationConsistency = + Binding extends ResourcePolicyBinding + ? { readonly resourcePolicy: Binding } + : { readonly resourcePolicy?: never } + export function defineWorkspaceOperation< const Id extends string, const Role extends PermissionType, const PrincipalKinds extends readonly PrincipalKind[], const DelegatedServices extends readonly DelegatedServiceId[] = readonly [], + const ResourcePolicy extends ResourcePolicyBinding | undefined = undefined, >( operation: WorkspaceOperation & WorkspaceApiKeyPrincipalConsistency & - DelegatedPrincipalConsistency + DelegatedPrincipalConsistency & + ResourcePolicyOperationConsistency ): WorkspaceOperation & - DelegatedPrincipalConsistency { + DelegatedPrincipalConsistency & + ResourcePolicyOperationConsistency { if (operation.principalKinds.length === 0) { throw new Error(`Operation ${operation.id} must allow at least one principal kind`) } @@ -94,8 +106,11 @@ export function defineWorkspaceOperation< throw new Error(`Operation ${operation.id} declares duplicate delegated services`) } + if (operation.resourcePolicy) requireResourcePolicyBinding(operation.resourcePolicy) + Object.freeze(operation.principalKinds) if (operation.delegatedServices) Object.freeze(operation.delegatedServices) + if (operation.resourcePolicy) Object.freeze(operation.resourcePolicy) Object.freeze(operation) return operation } diff --git a/apps/sim/lib/credential-groups/application/authorization.test.ts b/apps/sim/lib/credential-groups/application/authorization.test.ts index c07b7c6b032..052dfba31f9 100644 --- a/apps/sim/lib/credential-groups/application/authorization.test.ts +++ b/apps/sim/lib/credential-groups/application/authorization.test.ts @@ -1,18 +1,48 @@ /** * @vitest-environment node */ + import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { compileCredentialGroupWorkflowAccessPolicy } from '@/lib/credential-groups/application/workflow-access-policy' +import { credentialOperations } from '@/lib/credentials/application/operations' const mocks = vi.hoisted(() => ({ loadEnrollmentAccess: vi.fn(), + requirePolicy: vi.fn(), })) vi.mock('@/lib/credential-groups/credentials', () => ({ loadCredentialGroupEnrollmentAccessForSubject: mocks.loadEnrollmentAccess, })) -import { requireCredentialGroupEnrollmentAccess } from '@/lib/credential-groups/application/authorization' +vi.mock('@/lib/resource-policies/repository', () => ({ + requireResourcePolicy: mocks.requirePolicy, +})) + +import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + credentialGroupId: 'group-1', + credentialGroupEnrollmentId: 'enrollment-1', +} + +function storedPolicy(allowedWorkflowIds: string[] = []) { + return { + id: 'policy-1', + workspaceId: 'workspace-1', + revision: 1, + document: compileCredentialGroupWorkflowAccessPolicy({ + credentialGroupId: 'group-1', + allowedWorkflowIds, + }), + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } +} function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { return { @@ -25,12 +55,12 @@ function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { expiresAt: new Date(Date.now() + 60_000), delegationContext: { kind: 'workflow_execution', - workflowId: 'workflow-1', + workflowId: 'root-workflow', principal: { kind: 'system', serviceId: 'webhook', workspaceId: 'workspace-1', - workflowId: 'workflow-1', + workflowId: 'root-workflow', webhookId: 'webhook-1', provider: 'slack', subject: { @@ -40,25 +70,45 @@ function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { subjectId: 'U123', }, }, + currentWorkflow: { + workflowId: 'workflow-1', + mode: 'deployment', + deploymentVersionId: 'version-1', + }, }, } } -describe('requireCredentialGroupEnrollmentAccess', () => { +function requireAccess( + principal: WorkflowExecutionDelegatedPrincipal, + accessContext = context +): Promise { + return requireCredentialGroupCredentialAccess( + principal, + accessContext, + credentialOperations.useManagedOAuth.resourcePolicy + ) +} + +describe('requireCredentialGroupCredentialAccess', () => { beforeEach(() => { vi.clearAllMocks() + mocks.requirePolicy.mockResolvedValue(storedPolicy()) mocks.loadEnrollmentAccess.mockResolvedValue({ enrollmentId: 'enrollment-1', email: 'person@example.com', }) }) - it('resolves an external workflow actor to their enrollment', async () => { + it('allows an external actor to use only their own enrollment', async () => { const principal = executorPrincipal() - await expect(requireCredentialGroupEnrollmentAccess(principal, 'group-1')).resolves.toEqual({ - enrollmentId: 'enrollment-1', - email: 'person@example.com', + await expect(requireAccess(principal)).resolves.toBeUndefined() + expect(mocks.requirePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + codec: expect.objectContaining({ resourceType: 'credential_group' }), }) expect(mocks.loadEnrollmentAccess).toHaveBeenCalledWith('group-1', { kind: 'external_user', @@ -66,38 +116,103 @@ describe('requireCredentialGroupEnrollmentAccess', () => { tenantId: 'T123', subjectId: 'U123', }) + + await expect( + requireAccess(principal, { + ...context, + credentialGroupEnrollmentId: 'enrollment-2', + }) + ).rejects.toMatchObject({ code: 'forbidden' }) }) - it('rejects an actorless workflow principal', async () => { + it('allows a Sim actor to use their own enrollment', async () => { + const principal = executorPrincipal() + principal.subjectUserId = 'user-1' + principal.delegationContext!.principal = { + kind: 'session', + userId: 'user-1', + sessionId: 'session-1', + } + + await expect(requireAccess(principal)).resolves.toBeUndefined() + expect(mocks.loadEnrollmentAccess).toHaveBeenCalledWith('group-1', { + kind: 'sim_user', + userId: 'user-1', + }) + }) + + it('allows an actorless deployed workflow only when its current workflow is allowlisted', async () => { const principal = executorPrincipal() principal.delegationContext!.principal = { kind: 'system', serviceId: 'schedule', workspaceId: 'workspace-1', - workflowId: 'workflow-1', + workflowId: 'root-workflow', } + mocks.requirePolicy.mockResolvedValue(storedPolicy(['workflow-1'])) - await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') - ).rejects.toMatchObject({ + await expect(requireAccess(principal)).resolves.toBeUndefined() + expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + + principal.delegationContext!.currentWorkflow = { workflowId: 'workflow-1', mode: 'draft' } + await expect(requireAccess(principal)).rejects.toMatchObject({ code: 'forbidden', - message: 'Credential Group enrollment access required', }) - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() }) - it('rejects an executor delegation whose Sim subject does not match the workflow actor', async () => { + it('uses the current child workflow rather than the root workflow grant', async () => { const principal = executorPrincipal() - principal.subjectUserId = 'user-2' principal.delegationContext!.principal = { + kind: 'system', + serviceId: 'schedule', + workspaceId: 'workspace-1', + workflowId: 'root-workflow', + } + principal.delegationContext!.currentWorkflow = { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'child-version', + } + mocks.requirePolicy.mockResolvedValue(storedPolicy(['root-workflow'])) + + await expect(requireAccess(principal)).rejects.toMatchObject({ + code: 'forbidden', + }) + }) + + it('rejects inconsistent Sim and external subject assertions before loading policy', async () => { + const simPrincipal = executorPrincipal() + simPrincipal.subjectUserId = 'user-2' + simPrincipal.delegationContext!.principal = { kind: 'session', userId: 'user-1', sessionId: 'session-1', } + await expect(requireAccess(simPrincipal)).rejects.toMatchObject({ code: 'forbidden' }) - await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') - ).rejects.toMatchObject({ code: 'forbidden' }) + const externalPrincipal = executorPrincipal() + externalPrincipal.subjectUserId = 'invented-user' + await expect(requireAccess(externalPrincipal)).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.requirePolicy).not.toHaveBeenCalled() + }) + + it('requires the original principal and current workflow before loading policy', async () => { + const missingPrincipal = executorPrincipal() + missingPrincipal.delegationContext!.principal = undefined + await expect(requireAccess(missingPrincipal)).rejects.toThrow('missing its workflow principal') + + const missingCurrentWorkflow = executorPrincipal() + missingCurrentWorkflow.delegationContext!.currentWorkflow = undefined + await expect(requireAccess(missingCurrentWorkflow)).rejects.toThrow( + 'missing its current workflow authority' + ) + expect(mocks.requirePolicy).not.toHaveBeenCalled() + }) + + it('loads and validates the required policy before resolving actor enrollment', async () => { + mocks.requirePolicy.mockRejectedValue(new Error('Malformed resource policy')) + + await expect(requireAccess(executorPrincipal())).rejects.toThrow('Malformed resource policy') expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/credential-groups/application/authorization.ts b/apps/sim/lib/credential-groups/application/authorization.ts index 553f522aeed..2cde1459083 100644 --- a/apps/sim/lib/credential-groups/application/authorization.ts +++ b/apps/sim/lib/credential-groups/application/authorization.ts @@ -1,74 +1,112 @@ import { type Principal, - requirePrincipalSubjectUserId, resolvePrincipalSubject, - type WorkflowExecutionDelegatedPrincipal, + type WorkflowExecutionAuthority, + type WorkflowExecutionPrincipal, } from '@sim/auth/principal' import type { WorkspaceAuthorizationContext, WorkspaceDelegationPolicy, } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' -import type { CredentialGroupCredentialListContext } from '@/lib/credential-groups/credentials' import { - type CredentialGroupEnrollmentAccess, - loadCredentialGroupEnrollmentAccessForSubject, -} from '@/lib/credential-groups/credentials' + credentialGroupWorkflowAccessPolicyCodec, + evaluateCredentialGroupWorkflowAccess, +} from '@/lib/credential-groups/application/workflow-access-policy' +import type { CredentialGroupCredentialListContext } from '@/lib/credential-groups/credentials' +import { loadCredentialGroupEnrollmentAccessForSubject } from '@/lib/credential-groups/credentials' +import type { ResourcePolicyBindingFor } from '@/lib/resource-policies/registry' +import { requireResourcePolicy } from '@/lib/resource-policies/repository' export const CREDENTIAL_GROUP_DELEGATION_AUDIENCE = 'sim:credential-groups' -export interface CredentialGroupApplicationContext - extends WorkspaceAuthorizationContext, - CredentialGroupCredentialListContext { - enrollmentAccess?: CredentialGroupEnrollmentAccess +export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizationContext { + credentialGroupId: string } -function requireWorkflowExecutionPrincipal(principal: Principal) { +export interface CredentialGroupApplicationContext + extends CredentialGroupAuthorizationContext, + CredentialGroupCredentialListContext {} + +function requireWorkflowExecutionPrincipal(principal: Principal): WorkflowExecutionPrincipal { if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { throw new Error('Credential Group use requires an executor delegation') } - const executionPrincipal = (principal as WorkflowExecutionDelegatedPrincipal).delegationContext - ?.principal + const executionPrincipal = principal.delegationContext?.principal if (!executionPrincipal) { throw new Error('Executor delegation is missing its workflow principal') } return executionPrincipal } -export function requireCredentialGroupWorkflowSubject(principal: Principal): string { - const executionPrincipal = requireWorkflowExecutionPrincipal(principal) - let subjectUserId: string - try { - subjectUserId = requirePrincipalSubjectUserId(executionPrincipal) - } catch { - throw new OrchestrationError('forbidden', 'Credential Group user access required') +function requireCurrentWorkflow(principal: Principal): WorkflowExecutionAuthority { + if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { + throw new Error('Credential Group use requires an executor delegation') } - if (principal.kind !== 'delegated' || principal.subjectUserId !== subjectUserId) { - throw new OrchestrationError('forbidden', 'Credential Group user access required') + const currentWorkflow = principal.delegationContext?.currentWorkflow + if (!currentWorkflow) { + throw new Error('Executor delegation is missing its current workflow authority') } - return subjectUserId + return currentWorkflow } -export async function requireCredentialGroupEnrollmentAccess( +function requireConsistentWorkflowSubject( principal: Principal, - credentialGroupId: string -): Promise { - const executionPrincipal = requireWorkflowExecutionPrincipal(principal) + executionPrincipal: WorkflowExecutionPrincipal +) { + if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { + throw new Error('Credential Group use requires an executor delegation') + } const subject = resolvePrincipalSubject(executionPrincipal) - if (!subject) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + if ( + (subject?.kind === 'sim_user' && principal.subjectUserId !== subject.userId) || + (subject?.kind !== 'sim_user' && principal.subjectUserId !== undefined) + ) { + throw new OrchestrationError('forbidden', 'Credential Group actor access required') } + return subject +} + +export function requireCredentialGroupWorkflowSubject(principal: Principal): string { + const subject = resolvePrincipalSubject(requireWorkflowExecutionPrincipal(principal)) if ( - subject.kind === 'sim_user' && - (principal.kind !== 'delegated' || principal.subjectUserId !== subject.userId) + subject?.kind !== 'sim_user' || + principal.kind !== 'delegated' || + principal.subjectUserId !== subject.userId ) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + throw new OrchestrationError('forbidden', 'Credential Group user access required') } - const access = await loadCredentialGroupEnrollmentAccessForSubject(credentialGroupId, subject) - if (!access) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + return subject.userId +} + +export async function requireCredentialGroupCredentialAccess( + principal: Principal, + context: CredentialGroupAuthorizationContext & { credentialGroupEnrollmentId: string }, + resourcePolicy: ResourcePolicyBindingFor<'credential_group'> +): Promise { + const executionPrincipal = requireWorkflowExecutionPrincipal(principal) + const currentWorkflow = requireCurrentWorkflow(principal) + const subject = requireConsistentWorkflowSubject(principal, executionPrincipal) + const policy = await requireResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + codec: credentialGroupWorkflowAccessPolicyCodec, + }) + const actorAccess = subject + ? await loadCredentialGroupEnrollmentAccessForSubject(context.credentialGroupId, subject) + : null + const decision = evaluateCredentialGroupWorkflowAccess({ + document: policy.document, + credentialGroupId: context.credentialGroupId, + selectedEnrollmentId: context.credentialGroupEnrollmentId, + ...(actorAccess ? { actorEnrollmentId: actorAccess.enrollmentId } : {}), + currentWorkflow, + resourcePolicy, + }) + if (decision.decision !== 'allow') { + throw new OrchestrationError('forbidden', 'Credential Group credential access denied') } - return access } export const credentialGroupDelegationPolicy = { diff --git a/apps/sim/lib/credential-groups/application/manage-access.test.ts b/apps/sim/lib/credential-groups/application/manage-access.test.ts new file mode 100644 index 00000000000..b333ef02b0c --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.test.ts @@ -0,0 +1,274 @@ +/** + * @vitest-environment node + */ + +import type { SessionPrincipal } from '@sim/auth/principal' +import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + compileCredentialGroupWorkflowAccessPolicy, + credentialGroupWorkflowAccessPolicyCodec, + decodeCredentialGroupWorkflowAccessPolicy, +} from '@/lib/credential-groups/application/workflow-access-policy' +import { CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT } from '@/lib/credential-groups/workflow-access-limits' + +const mocks = vi.hoisted(() => ({ + requirePolicy: vi.fn(), + requireAvailability: vi.fn(), + resolveGroup: vi.fn(), + resolvePermission: vi.fn(), + writePolicy: vi.fn(), +})) + +vi.mock('@/lib/credential-groups/application/context', () => ({ + requireCredentialGroupSettingsAvailable: mocks.requireAvailability, + resolveCredentialGroupSettingsContext: mocks.resolveGroup, +})) + +vi.mock('@sim/platform-authz/workspace', () => ({ + permissionSatisfies: (permission: string | null, required: string) => + permission === 'admin' || permission === required, + resolveEffectiveWorkspacePermission: mocks.resolvePermission, +})) + +vi.mock('@/lib/resource-policies/repository', () => { + class ResourcePolicyRevisionConflictError extends Error {} + class ResourcePolicyNotFoundError extends Error {} + return { + requireResourcePolicy: mocks.requirePolicy, + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, + writeResourcePolicy: mocks.writePolicy, + } +}) + +import { + readCredentialGroupAccess, + updateCredentialGroupAccess, +} from '@/lib/credential-groups/application/manage-access' +import { + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, +} from '@/lib/resource-policies/repository' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: 'organization-1', + allowPersonalApiKeys: true, + billedAccountUserId: 'billing-owner-1', + credentialGroupId: 'group-1', + name: 'Support', + status: 'active' as const, + options: [], +} +const principal: SessionPrincipal = { + kind: 'session', + userId: 'admin-1', + sessionId: 'session-1', +} +const target = { + assertedWorkspaceId: 'workspace-1', + credentialGroupId: 'group-1', +} + +function document(allowedWorkflowIds: string[] = []) { + return compileCredentialGroupWorkflowAccessPolicy({ + credentialGroupId: context.credentialGroupId, + allowedWorkflowIds, + }) +} + +function storedPolicy(revision = 1, policyDocument = document(['workflow-1', 'workflow-2'])) { + return { + id: 'policy-1', + workspaceId: 'workspace-1', + revision, + document: policyDocument, + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } +} + +const WORKFLOWS = [ + { id: 'workflow-1', name: 'Support workflow', nameLength: 16 }, + { id: 'workflow-2', name: 'Finance workflow', nameLength: 16 }, +] + +describe('Credential Group workflow access operations', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.resolveGroup.mockResolvedValue(context) + mocks.resolvePermission.mockResolvedValue('admin') + mocks.requireAvailability.mockResolvedValue(undefined) + mocks.requirePolicy.mockResolvedValue(storedPolicy()) + queueTableRows(schemaMock.workflow, WORKFLOWS) + }) + + it('returns the unchanged workflow access wire shape', async () => { + await expect(readCredentialGroupAccess.execute({ principal, input: target })).resolves.toEqual({ + revision: 1, + allowedWorkflowIds: ['workflow-1', 'workflow-2'], + workflows: [ + { id: 'workflow-1', name: 'Support workflow' }, + { id: 'workflow-2', name: 'Finance workflow' }, + ], + }) + expect(mocks.requirePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + codec: credentialGroupWorkflowAccessPolicyCodec, + }) + expect(dbChainMockFns.limit).toHaveBeenCalledWith(CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT + 1) + }) + + it('fails before loading the catalog when stored policy is noncanonical', async () => { + mocks.requirePolicy.mockResolvedValue( + storedPolicy(1, { + ...document([]), + statements: [ + { + ...document(['workflow-1']).statements[0], + sid: 'OlderWorkflowGrant', + }, + ], + }) + ) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow() + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + + it('fails fast when policy storage is missing', async () => { + mocks.requirePolicy.mockRejectedValue( + new ResourcePolicyNotFoundError('credential_group', 'group-1') + ) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow() + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + + it('requires settings availability before reading policy storage', async () => { + mocks.requireAvailability.mockRejectedValue(new Error('Credential Groups are not available')) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow( + 'Credential Groups are not available' + ) + expect(mocks.requirePolicy).not.toHaveBeenCalled() + }) + + it('fails when stored access references an archived or unavailable workflow', async () => { + resetDbChainMock() + queueTableRows(schemaMock.workflow, [WORKFLOWS[0]]) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow( + 'references unavailable workflow workflow-2' + ) + }) + + it('fails closed when the bounded workflow catalog overflows', async () => { + resetDbChainMock() + queueTableRows( + schemaMock.workflow, + Array.from({ length: CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT + 1 }, (_, index) => ({ + id: `workflow-${index}`, + name: `Workflow ${index}`, + nameLength: `Workflow ${index}`.length, + })) + ) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow( + `exceeds the ${CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT} row limit` + ) + }) + + it('requires current workspace-admin permission', async () => { + mocks.resolvePermission.mockResolvedValue('write') + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, allowedWorkflowIds: ['workflow-1'] }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.writePolicy).not.toHaveBeenCalled() + }) + + it('compiles, validates, and persists one canonical deployment-only statement', async () => { + const canonicalDocument = document(['workflow-2', 'workflow-1']) + mocks.writePolicy.mockResolvedValue(storedPolicy(2, canonicalDocument)) + + const result = await updateCredentialGroupAccess.execute({ + principal, + input: { + ...target, + expectedRevision: 1, + allowedWorkflowIds: ['workflow-2', 'workflow-1'], + }, + }) + + expect(mocks.writePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + expectedRevision: 1, + actorUserId: 'admin-1', + document: canonicalDocument, + codec: credentialGroupWorkflowAccessPolicyCodec, + }) + expect(result).toEqual({ + revision: 2, + allowedWorkflowIds: ['workflow-1', 'workflow-2'], + }) + expect( + decodeCredentialGroupWorkflowAccessPolicy(canonicalDocument, context.credentialGroupId) + ).toEqual(result.allowedWorkflowIds) + }) + + it('rejects duplicate and unavailable workflow selections before writes', async () => { + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { + ...target, + expectedRevision: 1, + allowedWorkflowIds: ['workflow-1', 'workflow-1'], + }, + }) + ).rejects.toThrow('repeats workflow workflow-1') + expect(mocks.writePolicy).not.toHaveBeenCalled() + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, allowedWorkflowIds: ['workflow-3'] }, + }) + ).rejects.toMatchObject({ code: 'validation', message: 'Policy workflow was not found' }) + expect(mocks.writePolicy).not.toHaveBeenCalled() + }) + + it('rejects a stale revision before loading workflow references', async () => { + mocks.requirePolicy.mockResolvedValue(storedPolicy(2)) + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, allowedWorkflowIds: ['workflow-1'] }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + expect(mocks.writePolicy).not.toHaveBeenCalled() + }) + + it('maps optimistic-write conflicts to an application conflict', async () => { + mocks.writePolicy.mockRejectedValue(new ResourcePolicyRevisionConflictError()) + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, allowedWorkflowIds: ['workflow-1'] }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + }) +}) diff --git a/apps/sim/lib/credential-groups/application/manage-access.ts b/apps/sim/lib/credential-groups/application/manage-access.ts new file mode 100644 index 00000000000..dbbbcd63c69 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.ts @@ -0,0 +1,189 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { db } from '@sim/db' +import { workflow } from '@sim/db/schema' +import { and, asc, eq, isNull, sql } from 'drizzle-orm' +import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { + requireCredentialGroupSettingsAvailable, + resolveCredentialGroupSettingsContext, +} from '@/lib/credential-groups/application/context' +import { credentialGroupOperations } from '@/lib/credential-groups/application/operations' +import { + compileCredentialGroupWorkflowAccessPolicy, + credentialGroupWorkflowAccessPolicyCodec, + decodeCredentialGroupWorkflowAccessPolicy, +} from '@/lib/credential-groups/application/workflow-access-policy' +import { + CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT, + CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH, +} from '@/lib/credential-groups/workflow-access-limits' +import { + ResourcePolicyRevisionConflictError, + requireResourcePolicy, + writeResourcePolicy, +} from '@/lib/resource-policies/repository' + +interface CredentialGroupAccessTargetInput { + assertedWorkspaceId: string + credentialGroupId: string +} + +export interface CredentialGroupWorkflowReference { + id: string + name: string +} + +async function loadCredentialGroupWorkflowCatalog( + workspaceId: string +): Promise { + const rows = await db + .select({ + id: workflow.id, + name: sql`left(${workflow.name}, ${CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH})`, + nameLength: sql`char_length(${workflow.name})`, + }) + .from(workflow) + .where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt))) + .orderBy(asc(workflow.name), asc(workflow.id)) + .limit(CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT + 1) + + if (rows.length > CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT) { + throw new Error( + `Credential Group workflow catalog exceeds the ${CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT} row limit` + ) + } + return rows.map((row) => { + if (!Number.isInteger(row.nameLength) || row.nameLength < 0) { + throw new Error(`Workflow ${row.id} returned an invalid name length`) + } + if (row.nameLength > CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH) { + throw new Error( + `Workflow ${row.id} name exceeds the ${CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH} character limit` + ) + } + return { id: row.id, name: row.name } + }) +} + +function requireAvailableWorkflows( + allowedWorkflowIds: readonly string[], + workflows: readonly CredentialGroupWorkflowReference[], + error: 'stored' | 'input' +): void { + const availableWorkflowIds = new Set(workflows.map((workflow) => workflow.id)) + for (const workflowId of allowedWorkflowIds) { + if (availableWorkflowIds.has(workflowId)) continue + if (error === 'input') { + throw new OrchestrationError('validation', 'Policy workflow was not found') + } + throw new Error( + `Credential Group workflow access references unavailable workflow ${workflowId}` + ) + } +} + +function presentPolicy( + policy: Awaited>, + credentialGroupId: string +): { + revision: number + allowedWorkflowIds: string[] +} { + return { + revision: policy.revision, + allowedWorkflowIds: decodeCredentialGroupWorkflowAccessPolicy( + policy.document, + credentialGroupId + ), + } +} + +export const readCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ + operation: credentialGroupOperations.readAccess, + resolveContext: ({ input }: { input: CredentialGroupAccessTargetInput }) => + resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), + authorizationOptions: {}, + async execute({ context }) { + await requireCredentialGroupSettingsAvailable(context.workspaceId) + const policy = await requireResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + codec: credentialGroupWorkflowAccessPolicyCodec, + }) + const access = presentPolicy(policy, context.credentialGroupId) + const workflows = await loadCredentialGroupWorkflowCatalog(context.workspaceId) + requireAvailableWorkflows(access.allowedWorkflowIds, workflows, 'stored') + return { + ...access, + workflows, + } + }, +}) + +export interface UpdateCredentialGroupAccessInput extends CredentialGroupAccessTargetInput { + expectedRevision: number + allowedWorkflowIds: string[] +} + +export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ + operation: credentialGroupOperations.updateAccess, + resolveContext: ({ input }: { input: UpdateCredentialGroupAccessInput }) => + resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), + authorizationOptions: {}, + async execute({ principal, input, context }) { + await requireCredentialGroupSettingsAvailable(context.workspaceId) + if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) { + throw new OrchestrationError('validation', 'Expected policy revision must be positive') + } + const existingPolicy = await requireResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + codec: credentialGroupWorkflowAccessPolicyCodec, + }) + if (existingPolicy.revision !== input.expectedRevision) { + throw new OrchestrationError('conflict', new ResourcePolicyRevisionConflictError().message) + } + decodeCredentialGroupWorkflowAccessPolicy(existingPolicy.document, context.credentialGroupId) + const document = compileCredentialGroupWorkflowAccessPolicy({ + credentialGroupId: context.credentialGroupId, + allowedWorkflowIds: input.allowedWorkflowIds, + }) + if (input.allowedWorkflowIds.length > 0) { + const workflows = await loadCredentialGroupWorkflowCatalog(context.workspaceId) + requireAvailableWorkflows(input.allowedWorkflowIds, workflows, 'input') + } + try { + return presentPolicy( + await writeResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + expectedRevision: input.expectedRevision, + actorUserId: principal.userId, + document, + codec: credentialGroupWorkflowAccessPolicyCodec, + }), + context.credentialGroupId + ) + } catch (error) { + if (error instanceof ResourcePolicyRevisionConflictError) { + throw new OrchestrationError('conflict', error.message) + } + throw error + } + }, + projectAudit: ({ result, context }) => ({ + action: AuditAction.CREDENTIAL_GROUP_UPDATED, + resourceType: AuditResourceType.CREDENTIAL_GROUP, + resourceId: context.credentialGroupId, + resourceName: context.name, + description: 'Updated Credential Group workflow access', + metadata: { + revision: result.revision, + workflowCount: result.allowedWorkflowIds.length, + }, + }), +}) diff --git a/apps/sim/lib/credential-groups/application/operations.ts b/apps/sim/lib/credential-groups/application/operations.ts index ac90f261db2..9339f5dfd9f 100644 --- a/apps/sim/lib/credential-groups/application/operations.ts +++ b/apps/sim/lib/credential-groups/application/operations.ts @@ -25,6 +25,18 @@ export const credentialGroupOperations = { workspaceApiKey: 'deny', principalKinds: ['session'], }), + readAccess: defineWorkspaceOperation({ + id: 'credential_groups.access.read', + minimumRole: 'admin', + workspaceApiKey: 'deny', + principalKinds: ['session'], + }), + updateAccess: defineWorkspaceOperation({ + id: 'credential_groups.access.update', + minimumRole: 'admin', + workspaceApiKey: 'deny', + principalKinds: ['session'], + }), delete: defineWorkspaceOperation({ id: 'credential_groups.delete', minimumRole: 'admin', diff --git a/apps/sim/lib/credential-groups/application/workflow-access-policy.test.ts b/apps/sim/lib/credential-groups/application/workflow-access-policy.test.ts new file mode 100644 index 00000000000..abd5feb5c50 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/workflow-access-policy.test.ts @@ -0,0 +1,274 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { + compileCredentialGroupWorkflowAccessPolicy, + credentialGroupWorkflowAccessPolicyCodec, + decodeCredentialGroupWorkflowAccessPolicy, + evaluateCredentialGroupWorkflowAccess, + requireDefaultCredentialGroupWorkflowAccessPolicy, +} from '@/lib/credential-groups/application/workflow-access-policy' +import { CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT } from '@/lib/credential-groups/workflow-access-limits' +import type { ResourcePolicyBindingFor } from '@/lib/resource-policies/registry' + +const GROUP_ID = 'group-1' +const RESOURCE_POLICY = { + resourceType: 'credential_group', + action: 'credential_groups.credentials.use', +} as const satisfies ResourcePolicyBindingFor<'credential_group'> + +function policy(workflowIds: string[]) { + return compileCredentialGroupWorkflowAccessPolicy({ + credentialGroupId: GROUP_ID, + allowedWorkflowIds: workflowIds, + }) +} + +describe('Credential Group workflow access policy', () => { + it('compiles actor ownership plus one deterministic deployment-only workflow statement', () => { + expect(policy(['workflow-2', 'workflow-1'])).toEqual({ + version: 1, + resource: { type: 'credential_group', id: GROUP_ID }, + statements: [ + { + sid: 'CredentialGroupActorCredentialAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'credential_group_actor' }], + condition: { + Bool: { 'credential_group:ActorOwnsCredential': true }, + }, + }, + { + sid: 'WorkflowCredentialAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [ + { type: 'workflow', workflowId: 'workflow-1' }, + { type: 'workflow', workflowId: 'workflow-2' }, + ], + condition: { StringEquals: { 'execution:WorkflowMode': 'deployment' } }, + }, + ], + }) + expect(policy([]).statements).toEqual([ + { + sid: 'CredentialGroupActorCredentialAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'credential_group_actor' }], + condition: { + Bool: { 'credential_group:ActorOwnsCredential': true }, + }, + }, + ]) + }) + + it('rejects malformed workflow selections instead of normalizing them', () => { + expect(() => policy(['workflow-1', 'workflow-1'])).toThrow('repeats workflow workflow-1') + expect(() => policy([' workflow-1'])).toThrow('canonical non-empty strings') + expect(() => + policy( + Array.from( + { length: CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT + 1 }, + (_, index) => `workflow-${index}` + ) + ) + ).toThrow(`cannot allow more than ${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} workflows`) + }) + + it('decodes only the exact canonical document', () => { + const document = policy(['workflow-2', 'workflow-1']) + expect(decodeCredentialGroupWorkflowAccessPolicy(document, GROUP_ID)).toEqual([ + 'workflow-1', + 'workflow-2', + ]) + expect(() => decodeCredentialGroupWorkflowAccessPolicy(document, 'group-2')).toThrow( + 'does not match its canonical resource' + ) + }) + + it.each([ + ['a missing actor statement', { statements: [] }], + ['a workflow-only document', { statements: [policy(['workflow-1']).statements[1]] }], + [ + 'multiple workflow statements', + { + statements: [ + policy([]).statements[0], + policy(['workflow-1']).statements[1], + policy(['workflow-2']).statements[1], + ], + }, + ], + [ + 'a different actor SID', + { statements: [{ ...policy([]).statements[0], sid: 'OlderActorGrant' }] }, + ], + [ + 'a different actor condition', + { + statements: [ + { + ...policy([]).statements[0], + condition: { Bool: { 'credential_group:ActorOwnsCredential': false } }, + }, + ], + }, + ], + [ + 'a different SID', + { + statements: [ + policy([]).statements[0], + { ...policy(['workflow-1']).statements[1], sid: 'OlderGrant' }, + ], + }, + ], + [ + 'a deny', + { + statements: [ + policy([]).statements[0], + { ...policy(['workflow-1']).statements[1], effect: 'deny' }, + ], + }, + ], + [ + 'another action', + { + statements: [ + policy([]).statements[0], + { ...policy(['workflow-1']).statements[1], actions: ['other'] }, + ], + }, + ], + [ + 'a non-workflow principal', + { + statements: [ + policy([]).statements[0], + { + ...policy(['workflow-1']).statements[1], + principals: [{ type: 'user', userId: 'user-1' }], + }, + ], + }, + ], + [ + 'a non-scalar deployment condition', + { + statements: [ + policy([]).statements[0], + { + ...policy(['workflow-1']).statements[1], + condition: { StringEquals: { 'execution:WorkflowMode': ['deployment'] } }, + }, + ], + }, + ], + [ + 'unsorted workflow principals', + { + statements: [ + policy([]).statements[0], + { + ...policy(['workflow-1']).statements[1], + principals: [ + { type: 'workflow', workflowId: 'workflow-2' }, + { type: 'workflow', workflowId: 'workflow-1' }, + ], + }, + ], + }, + ], + ])('rejects %s', (_name, replacement) => { + const candidate = { ...policy([]), ...replacement } + expect(() => + credentialGroupWorkflowAccessPolicyCodec.parse(candidate, { + type: 'credential_group', + id: GROUP_ID, + }) + ).toThrow() + }) + + it('requires the trigger-created policy to be revision one with only actor access', () => { + expect(() => + requireDefaultCredentialGroupWorkflowAccessPolicy({ + revision: 1, + document: policy([]), + credentialGroupId: GROUP_ID, + }) + ).not.toThrow() + expect(() => + requireDefaultCredentialGroupWorkflowAccessPolicy({ + revision: 2, + document: policy([]), + credentialGroupId: GROUP_ID, + }) + ).toThrow('non-default') + expect(() => + requireDefaultCredentialGroupWorkflowAccessPolicy({ + revision: 1, + document: policy(['workflow-1']), + credentialGroupId: GROUP_ID, + }) + ).toThrow('non-default') + }) + + it('evaluates actor ownership and deployed workflow access through registered statements', () => { + const document = policy(['workflow-1']) + expect( + evaluateCredentialGroupWorkflowAccess({ + document, + credentialGroupId: GROUP_ID, + selectedEnrollmentId: 'enrollment-1', + actorEnrollmentId: 'enrollment-1', + currentWorkflow: { workflowId: 'workflow-2', mode: 'draft' }, + resourcePolicy: RESOURCE_POLICY, + }) + ).toEqual({ + decision: 'allow', + statementSid: 'CredentialGroupActorCredentialAccess', + }) + expect( + evaluateCredentialGroupWorkflowAccess({ + document, + credentialGroupId: GROUP_ID, + selectedEnrollmentId: 'enrollment-2', + actorEnrollmentId: 'enrollment-1', + currentWorkflow: { + workflowId: 'workflow-1', + mode: 'deployment', + deploymentVersionId: 'version-1', + }, + resourcePolicy: RESOURCE_POLICY, + }) + ).toEqual({ decision: 'allow', statementSid: 'WorkflowCredentialAccess' }) + expect( + evaluateCredentialGroupWorkflowAccess({ + document, + credentialGroupId: GROUP_ID, + selectedEnrollmentId: 'enrollment-2', + actorEnrollmentId: 'enrollment-1', + currentWorkflow: { workflowId: 'workflow-1', mode: 'draft' }, + resourcePolicy: RESOURCE_POLICY, + }) + ).toEqual({ decision: 'implicit_deny' }) + expect( + evaluateCredentialGroupWorkflowAccess({ + document, + credentialGroupId: GROUP_ID, + selectedEnrollmentId: 'enrollment-2', + currentWorkflow: { + workflowId: 'workflow-2', + mode: 'deployment', + deploymentVersionId: 'version-2', + }, + resourcePolicy: RESOURCE_POLICY, + }) + ).toEqual({ decision: 'implicit_deny' }) + }) +}) diff --git a/apps/sim/lib/credential-groups/application/workflow-access-policy.ts b/apps/sim/lib/credential-groups/application/workflow-access-policy.ts new file mode 100644 index 00000000000..f0e3248ceac --- /dev/null +++ b/apps/sim/lib/credential-groups/application/workflow-access-policy.ts @@ -0,0 +1,242 @@ +import type { WorkflowExecutionAuthority } from '@sim/auth/principal' +import { z } from 'zod' +import { CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT } from '@/lib/credential-groups/workflow-access-limits' +import { CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY } from '@/lib/resource-policies/conditions' +import { WORKFLOW_MODE_RESOURCE_POLICY_CONDITION_KEY } from '@/lib/resource-policies/conditions/workflow-mode' +import { + evaluateResourcePolicy, + type ResourcePolicyDecision, +} from '@/lib/resource-policies/evaluator' +import { + credentialGroupActorResourcePolicyPrincipalSchema, + workflowResourcePolicyPrincipalSchema, +} from '@/lib/resource-policies/principals' +import { + CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION, + type ResourcePolicyBindingFor, +} from '@/lib/resource-policies/registry' +import type { ResourcePolicyCodec } from '@/lib/resource-policies/types' + +export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_SID = 'WorkflowCredentialAccess' +export const CREDENTIAL_GROUP_ACTOR_ACCESS_SID = 'CredentialGroupActorCredentialAccess' +export { CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION } +export const CREDENTIAL_GROUP_WORKFLOW_MODE_CONDITION_KEY = + WORKFLOW_MODE_RESOURCE_POLICY_CONDITION_KEY + +const canonicalIdSchema = z + .string() + .min(1) + .max(128) + .refine((value) => value === value.trim(), 'Resource policy IDs must be canonical') + +const credentialGroupActorAccessStatementSchema = z + .object({ + sid: z.literal(CREDENTIAL_GROUP_ACTOR_ACCESS_SID), + effect: z.literal('allow'), + actions: z.tuple([z.literal(CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION)]), + principals: z.tuple([credentialGroupActorResourcePolicyPrincipalSchema]), + condition: z + .object({ + Bool: z + .object({ + [CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY]: z.literal(true), + }) + .strict(), + }) + .strict(), + }) + .strict() + +const credentialGroupWorkflowAccessStatementSchema = z + .object({ + sid: z.literal(CREDENTIAL_GROUP_WORKFLOW_ACCESS_SID), + effect: z.literal('allow'), + actions: z.tuple([z.literal(CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION)]), + principals: z + .array(workflowResourcePolicyPrincipalSchema) + .min(1) + .max(CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT) + .superRefine((principals, ctx) => { + for (let index = 1; index < principals.length; index += 1) { + const previous = principals[index - 1].workflowId + const current = principals[index].workflowId + if (current === previous) { + ctx.addIssue({ + code: 'custom', + path: [index, 'workflowId'], + message: `Credential Group access repeats workflow ${current}`, + }) + } else if (current < previous) { + ctx.addIssue({ + code: 'custom', + path: [index, 'workflowId'], + message: 'Credential Group workflow access principals must be sorted', + }) + } + } + }), + condition: z + .object({ + StringEquals: z + .object({ + [CREDENTIAL_GROUP_WORKFLOW_MODE_CONDITION_KEY]: z.literal('deployment'), + }) + .strict(), + }) + .strict(), + }) + .strict() + +export const credentialGroupWorkflowAccessPolicySchema = z + .object({ + version: z.literal(1), + resource: z + .object({ + type: z.literal('credential_group'), + id: canonicalIdSchema, + }) + .strict(), + statements: z.union([ + z.tuple([credentialGroupActorAccessStatementSchema]), + z.tuple([ + credentialGroupActorAccessStatementSchema, + credentialGroupWorkflowAccessStatementSchema, + ]), + ]), + }) + .strict() + +export type CredentialGroupWorkflowAccessPolicy = z.output< + typeof credentialGroupWorkflowAccessPolicySchema +> + +export const credentialGroupWorkflowAccessPolicyCodec = { + resourceType: 'credential_group', + parse( + value: unknown, + expected: { type: 'credential_group'; id: string } + ): CredentialGroupWorkflowAccessPolicy { + const document = credentialGroupWorkflowAccessPolicySchema.parse(value) + if (document.resource.type !== expected.type || document.resource.id !== expected.id) { + throw new Error('Resource policy document does not match its canonical resource') + } + return document + }, +} as const satisfies ResourcePolicyCodec<'credential_group', CredentialGroupWorkflowAccessPolicy> + +function requireAllowedWorkflowIds(allowedWorkflowIds: readonly string[]): string[] { + if (allowedWorkflowIds.length > CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT) { + throw new Error( + `Credential Group access cannot allow more than ${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} workflows` + ) + } + + const workflowIds = new Set() + for (const workflowId of allowedWorkflowIds) { + if (!workflowId.trim() || workflowId !== workflowId.trim() || workflowId.length > 128) { + throw new Error('Credential Group access workflow IDs must be canonical non-empty strings') + } + if (workflowIds.has(workflowId)) { + throw new Error(`Credential Group access repeats workflow ${workflowId}`) + } + workflowIds.add(workflowId) + } + return [...workflowIds].sort() +} + +export function compileCredentialGroupWorkflowAccessPolicy(input: { + credentialGroupId: string + allowedWorkflowIds: readonly string[] +}): CredentialGroupWorkflowAccessPolicy { + const allowedWorkflowIds = requireAllowedWorkflowIds(input.allowedWorkflowIds) + const actorStatement = { + sid: CREDENTIAL_GROUP_ACTOR_ACCESS_SID, + effect: 'allow', + actions: [CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION], + principals: [{ type: 'credential_group_actor' }], + condition: { + Bool: { + [CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY]: true, + }, + }, + } as const + return credentialGroupWorkflowAccessPolicyCodec.parse( + { + version: 1, + resource: { type: 'credential_group', id: input.credentialGroupId }, + statements: + allowedWorkflowIds.length === 0 + ? [actorStatement] + : [ + actorStatement, + { + sid: CREDENTIAL_GROUP_WORKFLOW_ACCESS_SID, + effect: 'allow', + actions: [CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION], + principals: allowedWorkflowIds.map((workflowId) => ({ + type: 'workflow' as const, + workflowId, + })), + condition: { + StringEquals: { + [CREDENTIAL_GROUP_WORKFLOW_MODE_CONDITION_KEY]: 'deployment', + }, + }, + }, + ], + }, + { type: 'credential_group', id: input.credentialGroupId } + ) +} + +export function decodeCredentialGroupWorkflowAccessPolicy( + document: unknown, + credentialGroupId: string +): string[] { + const canonical = credentialGroupWorkflowAccessPolicyCodec.parse(document, { + type: 'credential_group', + id: credentialGroupId, + }) + return canonical.statements.length === 1 + ? [] + : canonical.statements[1].principals.map((principal) => principal.workflowId) +} + +export function requireDefaultCredentialGroupWorkflowAccessPolicy(input: { + revision: number + document: CredentialGroupWorkflowAccessPolicy + credentialGroupId: string +}): void { + const allowedWorkflowIds = decodeCredentialGroupWorkflowAccessPolicy( + input.document, + input.credentialGroupId + ) + if (input.revision !== 1 || allowedWorkflowIds.length !== 0) { + throw new Error('New resource was bound to a non-default resource policy') + } +} + +export function evaluateCredentialGroupWorkflowAccess(input: { + document: CredentialGroupWorkflowAccessPolicy + credentialGroupId: string + selectedEnrollmentId: string + actorEnrollmentId?: string + currentWorkflow: WorkflowExecutionAuthority + resourcePolicy: ResourcePolicyBindingFor<'credential_group'> +}): ResourcePolicyDecision { + const document = credentialGroupWorkflowAccessPolicyCodec.parse(input.document, { + type: 'credential_group', + id: input.credentialGroupId, + }) + return evaluateResourcePolicy({ + document, + action: input.resourcePolicy.action, + facts: { + ...(input.actorEnrollmentId + ? { credentialGroupActorEnrollmentId: input.actorEnrollmentId } + : {}), + credentialGroupCredentialEnrollmentId: input.selectedEnrollmentId, + currentWorkflow: input.currentWorkflow, + }, + }) +} diff --git a/apps/sim/lib/credential-groups/service.test.ts b/apps/sim/lib/credential-groups/service.test.ts index c73afcc2bee..32badce9ee4 100644 --- a/apps/sim/lib/credential-groups/service.test.ts +++ b/apps/sim/lib/credential-groups/service.test.ts @@ -18,7 +18,11 @@ vi.mock('@/lib/credential-groups/provider-registry', () => ({ getCredentialGroupProviderAdapter: () => ({ getPolicy: mockGetPolicy }), })) -import { updateCredentialGroup } from '@/lib/credential-groups/service' +import { + createCredentialGroup, + deleteCredentialGroup, + updateCredentialGroup, +} from '@/lib/credential-groups/service' describe('Credential Group service', () => { beforeEach(() => { @@ -86,4 +90,95 @@ describe('Credential Group service', () => { } ) }) + + it('creates a group only when its trigger-created default policy is present', async () => { + const created = { + id: 'group-1', + workspaceId: 'workspace-1', + publicId: 'public-1', + name: 'Support accounts', + description: null, + options: [], + encryptedProviderConfiguration: null, + status: 'active' as const, + createdBy: 'user-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } + dbChainMockFns.returning.mockResolvedValueOnce([created]) + queueTableRows(schemaMock.resourcePolicy, [ + { + id: 'policy-1', + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + revision: 1, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'CredentialGroupActorCredentialAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'credential_group_actor' }], + condition: { + Bool: { 'credential_group:ActorOwnsCredential': true }, + }, + }, + ], + }, + createdAt: created.createdAt, + updatedAt: created.updatedAt, + }, + ]) + + await expect( + createCredentialGroup('workspace-1', 'user-1', { + name: 'Support accounts', + description: '', + options: [], + }) + ).resolves.toMatchObject({ id: 'group-1', workspaceId: 'workspace-1' }) + expect(dbChainMockFns.transaction).toHaveBeenCalledOnce() + }) + + it('rolls back group creation when the required policy is missing', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([ + { + id: 'group-1', + workspaceId: 'workspace-1', + publicId: 'public-1', + name: 'Support accounts', + description: null, + options: [], + encryptedProviderConfiguration: null, + status: 'active', + createdBy: 'user-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + }, + ]) + + await expect( + createCredentialGroup('workspace-1', 'user-1', { + name: 'Support accounts', + description: '', + options: [], + }) + ).rejects.toThrow('Required resource policy is missing') + }) + + it('deletes the policy and group in one locked transaction', async () => { + queueTableRows(schemaMock.credentialGroup, [{ id: 'group-1' }]) + dbChainMockFns.returning + .mockResolvedValueOnce([{ id: 'policy-1' }]) + .mockResolvedValueOnce([{ id: 'group-1' }]) + + await expect(deleteCredentialGroup('workspace-1', 'group-1')).resolves.toBe(true) + + expect(dbChainMockFns.for).toHaveBeenCalledWith('update') + expect(dbChainMockFns.delete).toHaveBeenCalledTimes(2) + expect(dbChainMockFns.transaction).toHaveBeenCalledOnce() + }) }) diff --git a/apps/sim/lib/credential-groups/service.ts b/apps/sim/lib/credential-groups/service.ts index 1ff48a9a7b1..6bff6ee700d 100644 --- a/apps/sim/lib/credential-groups/service.ts +++ b/apps/sim/lib/credential-groups/service.ts @@ -7,6 +7,10 @@ import { } from '@sim/db/schema' import { generateId } from '@sim/utils/id' import { and, desc, eq, inArray } from 'drizzle-orm' +import { + credentialGroupWorkflowAccessPolicyCodec, + requireDefaultCredentialGroupWorkflowAccessPolicy, +} from '@/lib/credential-groups/application/workflow-access-policy' import { credentialGroupScopePolicyVersion } from '@/lib/credential-groups/provider-adapter' import { decryptCredentialGroupProviderConfiguration } from '@/lib/credential-groups/provider-configuration' import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry' @@ -19,6 +23,10 @@ import type { UpdateCredentialGroupInput, } from '@/lib/credential-groups/types' import type { DbOrTx } from '@/lib/db/types' +import { + deleteResourcePolicyForResource, + requireResourcePolicy, +} from '@/lib/resource-policies/repository' function scopesEqual(left: string[], right: string[]): boolean { const normalizedLeft = [...new Set(left)].sort() @@ -166,35 +174,66 @@ export async function createCredentialGroup( ): Promise { const now = new Date() const options = await Promise.all(body.options.map((option) => buildOption(workspaceId, option))) - const [created] = await db - .insert(credentialGroup) - .values({ - id: generateId(), - workspaceId, - publicId: generateId(), - name: body.name, - description: body.description || null, - options, - status: 'active', - createdBy: userId, - createdAt: now, - updatedAt: now, - }) - .returning() + return db.transaction(async (tx) => { + const [created] = await tx + .insert(credentialGroup) + .values({ + id: generateId(), + workspaceId, + publicId: generateId(), + name: body.name, + description: body.description || null, + options, + status: 'active', + createdBy: userId, + createdAt: now, + updatedAt: now, + }) + .returning() - if (!created) throw new Error('Credential group insert returned no row') - return toCredentialGroup(created) + if (!created) throw new Error('Credential group insert returned no row') + const policy = await requireResourcePolicy( + { + workspaceId, + resourceType: 'credential_group', + resourceId: created.id, + codec: credentialGroupWorkflowAccessPolicyCodec, + }, + tx + ) + requireDefaultCredentialGroupWorkflowAccessPolicy({ + revision: policy.revision, + document: policy.document, + credentialGroupId: created.id, + }) + return toCredentialGroup(created) + }) } export async function deleteCredentialGroup( workspaceId: string, groupId: string ): Promise { - const deleted = await db - .delete(credentialGroup) - .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) - .returning({ id: credentialGroup.id }) - return deleted.length > 0 + return db.transaction(async (tx) => { + const [existing] = await tx + .select({ id: credentialGroup.id }) + .from(credentialGroup) + .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) + .limit(1) + .for('update') + if (!existing) return false + + await deleteResourcePolicyForResource( + { workspaceId, resourceType: 'credential_group', resourceId: groupId }, + tx + ) + const deleted = await tx + .delete(credentialGroup) + .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) + .returning({ id: credentialGroup.id }) + if (deleted.length !== 1) throw new Error('Locked Credential Group delete returned no row') + return true + }) } export async function updateCredentialGroup( diff --git a/apps/sim/lib/credential-groups/workflow-access-limits.ts b/apps/sim/lib/credential-groups/workflow-access-limits.ts new file mode 100644 index 00000000000..50c7c24949e --- /dev/null +++ b/apps/sim/lib/credential-groups/workflow-access-limits.ts @@ -0,0 +1,3 @@ +export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT = 50 +export const CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT = 500 +export const CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH = 255 diff --git a/apps/sim/lib/credentials/application/operations.ts b/apps/sim/lib/credentials/application/operations.ts index 43d7d531b40..4753c3136a8 100644 --- a/apps/sim/lib/credentials/application/operations.ts +++ b/apps/sim/lib/credentials/application/operations.ts @@ -1,5 +1,6 @@ import type { ApplicationOperation } from '@/lib/core/application' import { defineWorkspaceOperation, type WorkspaceOperation } from '@/lib/core/application' +import { CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION } from '@/lib/resource-policies/registry' export type CredentialRole = 'member' | 'admin' @@ -148,6 +149,10 @@ export const credentialOperations = { workspaceApiKey: 'deny', principalKinds: ['delegated'], delegatedServices: ['executor'], + resourcePolicy: { + resourceType: 'credential_group', + action: CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION, + }, }), } as const diff --git a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts index 7828cb946e1..2a7bed02cb3 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts @@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ loadContext: vi.fn(), - requireEnrollmentAccess: vi.fn(), + requireCredentialAccess: vi.fn(), resolvePermission: vi.fn(), resolveToken: vi.fn(), recordAudit: vi.fn(), @@ -18,7 +18,7 @@ vi.mock('@/lib/credentials/managed-oauth', () => ({ })) vi.mock('@/lib/credential-groups/application/authorization', () => ({ - requireCredentialGroupEnrollmentAccess: mocks.requireEnrollmentAccess, + requireCredentialGroupCredentialAccess: mocks.requireCredentialAccess, })) vi.mock('@sim/platform-authz/workspace', () => ({ @@ -66,6 +66,11 @@ function executorPrincipal(credentialId = 'credential-1'): WorkflowExecutionDele kind: 'workflow_execution', workflowId: 'workflow-1', principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + currentWorkflow: { + workflowId: 'workflow-1', + mode: 'deployment', + deploymentVersionId: 'version-1', + }, }, } } @@ -75,10 +80,7 @@ describe('resolveManagedOAuthCredentialToken', () => { vi.clearAllMocks() mocks.loadContext.mockResolvedValue(context) mocks.resolvePermission.mockResolvedValue('read') - mocks.requireEnrollmentAccess.mockResolvedValue({ - enrollmentId: 'enrollment-1', - email: 'person@example.com', - }) + mocks.requireCredentialAccess.mockResolvedValue(undefined) mocks.resolveToken.mockResolvedValue({ accessToken: 'access-token', refreshed: false }) }) @@ -106,14 +108,19 @@ describe('resolveManagedOAuthCredentialToken', () => { }) it('resolves the token only after current workspace authorization', async () => { + const principal = executorPrincipal() const result = await resolveManagedOAuthCredentialToken.execute({ - principal: executorPrincipal(), + principal, input, }) expect(mocks.resolvePermission).toHaveBeenCalledWith('user-1', 'workspace-1', null, undefined, { forUpdate: undefined, }) + expect(mocks.requireCredentialAccess).toHaveBeenCalledWith(principal, context, { + resourceType: 'credential_group', + action: 'credential_groups.credentials.use', + }) expect(mocks.resolveToken).toHaveBeenCalledWith({ credentialId: 'credential-1', workspaceId: 'workspace-1', @@ -124,18 +131,27 @@ describe('resolveManagedOAuthCredentialToken', () => { expect(mocks.recordAudit).toHaveBeenCalledOnce() }) - it('rejects a credential owned by another group enrollment', async () => { - mocks.requireEnrollmentAccess.mockResolvedValueOnce({ - enrollmentId: 'enrollment-2', - email: 'other@example.com', + it('does not resolve token material when the resource policy denies access', async () => { + mocks.requireCredentialAccess.mockRejectedValueOnce({ + code: 'forbidden', + message: 'Credential Group credential access denied', }) await expect( resolveManagedOAuthCredentialToken.execute({ principal: executorPrincipal(), input }) ).rejects.toMatchObject({ code: 'forbidden', - message: 'Credential Group enrollment access required', + message: 'Credential Group credential access denied', }) expect(mocks.resolveToken).not.toHaveBeenCalled() }) + + it('allows token resolution after any policy allow, including workflow-wide access', async () => { + mocks.requireCredentialAccess.mockResolvedValueOnce(undefined) + + await expect( + resolveManagedOAuthCredentialToken.execute({ principal: executorPrincipal(), input }) + ).resolves.toEqual({ accessToken: 'access-token', refreshed: false }) + expect(mocks.resolveToken).toHaveBeenCalledOnce() + }) }) diff --git a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts index 90b8f0386ac..418a0e455b5 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts @@ -1,7 +1,7 @@ import { AuditAction, AuditResourceType } from '@sim/audit' import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' -import { requireCredentialGroupEnrollmentAccess } from '@/lib/credential-groups/application/authorization' +import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' import { managedOAuthCredentialDelegationPolicy } from '@/lib/credentials/application/authorization' import { credentialOperations } from '@/lib/credentials/application/operations' import { @@ -25,14 +25,8 @@ export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCa return context }, authorizationOptions: { delegation: managedOAuthCredentialDelegationPolicy }, - async authorizeResource({ principal, context }) { - const access = await requireCredentialGroupEnrollmentAccess( - principal, - context.credentialGroupId - ) - if (access.enrollmentId !== context.credentialGroupEnrollmentId) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') - } + async authorizeResource({ principal, context, resourcePolicy }) { + await requireCredentialGroupCredentialAccess(principal, context, resourcePolicy) }, execute: async ({ input, context }): Promise => resolveManagedOAuthToken({ diff --git a/apps/sim/lib/resource-policies/conditions/credential-group-actor-owns-credential.ts b/apps/sim/lib/resource-policies/conditions/credential-group-actor-owns-credential.ts new file mode 100644 index 00000000000..bb8d7e84fa5 --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/credential-group-actor-owns-credential.ts @@ -0,0 +1,21 @@ +import { defineResourcePolicyCondition } from '@/lib/resource-policies/conditions/types' + +export const CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY = + 'credential_group:ActorOwnsCredential' as const + +export const credentialGroupActorOwnsCredentialConditionDefinition = defineResourcePolicyCondition({ + key: CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY, + label: 'Actor owns credential', + valueType: 'boolean', + operators: ['Bool'], + selector: { type: 'internal' }, + resolve: (facts) => { + if ( + facts.credentialGroupActorEnrollmentId === undefined || + facts.credentialGroupCredentialEnrollmentId === undefined + ) { + return undefined + } + return facts.credentialGroupActorEnrollmentId === facts.credentialGroupCredentialEnrollmentId + }, +}) diff --git a/apps/sim/lib/resource-policies/conditions/index.ts b/apps/sim/lib/resource-policies/conditions/index.ts new file mode 100644 index 00000000000..6796a069ac6 --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/index.ts @@ -0,0 +1,15 @@ +export { CREDENTIAL_GROUP_ACTOR_OWNS_CREDENTIAL_CONDITION_KEY } from '@/lib/resource-policies/conditions/credential-group-actor-owns-credential' +export { + getResourcePolicyConditionDefinition, + RESOURCE_POLICY_CONDITION_DEFINITIONS, + requireResourcePolicyConditionDefinition, +} from '@/lib/resource-policies/conditions/registry' +export type { + ResourcePolicyConditionDefinition, + ResourcePolicyConditionEvaluationFacts, + ResourcePolicyConditionKey, + ResourcePolicyConditionOperator, + ResourcePolicyConditionSelector, + ResourcePolicyConditionValueType, +} from '@/lib/resource-policies/conditions/types' +export { RESOURCE_POLICY_CONDITION_OPERATORS } from '@/lib/resource-policies/conditions/types' diff --git a/apps/sim/lib/resource-policies/conditions/registry.test.ts b/apps/sim/lib/resource-policies/conditions/registry.test.ts new file mode 100644 index 00000000000..b4aa346416e --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/registry.test.ts @@ -0,0 +1,56 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { + RESOURCE_POLICY_CONDITION_DEFINITIONS, + requireResourcePolicyConditionDefinition, +} from '@/lib/resource-policies/conditions' + +describe('resource policy condition registry', () => { + it('registers workflow mode resolution and selector metadata together', () => { + const definition = RESOURCE_POLICY_CONDITION_DEFINITIONS['execution:WorkflowMode'] + expect(definition.operators).toEqual(['StringEquals']) + expect(definition.selector).toEqual({ + type: 'static', + options: [ + { value: 'draft', label: 'Draft' }, + { value: 'deployment', label: 'Deployed' }, + ], + }) + expect( + definition.resolve({ + currentWorkflow: { workflowId: 'workflow-1', mode: 'deployment' }, + }) + ).toBe('deployment') + }) + + it('registers Credential Group actor ownership as an internal Boolean fact', () => { + const definition = RESOURCE_POLICY_CONDITION_DEFINITIONS['credential_group:ActorOwnsCredential'] + expect(definition.valueType).toBe('boolean') + expect(definition.operators).toEqual(['Bool']) + expect(definition.selector).toEqual({ type: 'internal' }) + expect( + definition.resolve({ + credentialGroupActorEnrollmentId: 'enrollment-1', + credentialGroupCredentialEnrollmentId: 'enrollment-1', + }) + ).toBe(true) + expect( + definition.resolve({ + credentialGroupActorEnrollmentId: 'enrollment-1', + credentialGroupCredentialEnrollmentId: 'enrollment-2', + }) + ).toBe(false) + expect(definition.resolve({ credentialGroupCredentialEnrollmentId: 'enrollment-1' })).toBe( + undefined + ) + }) + + it('fails fast for an unregistered condition key', () => { + expect(() => requireResourcePolicyConditionDefinition('execution:Unknown')).toThrow( + 'Resource policy condition key execution:Unknown is not registered' + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/conditions/registry.ts b/apps/sim/lib/resource-policies/conditions/registry.ts new file mode 100644 index 00000000000..454ede858a3 --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/registry.ts @@ -0,0 +1,26 @@ +import { credentialGroupActorOwnsCredentialConditionDefinition } from '@/lib/resource-policies/conditions/credential-group-actor-owns-credential' +import type { + ResourcePolicyConditionDefinition, + ResourcePolicyConditionKey, +} from '@/lib/resource-policies/conditions/types' +import { workflowModeResourcePolicyConditionDefinition } from '@/lib/resource-policies/conditions/workflow-mode' + +export const RESOURCE_POLICY_CONDITION_DEFINITIONS = Object.freeze({ + 'credential_group:ActorOwnsCredential': credentialGroupActorOwnsCredentialConditionDefinition, + 'execution:WorkflowMode': workflowModeResourcePolicyConditionDefinition, +} as const satisfies Record) + +export function getResourcePolicyConditionDefinition( + key: ResourcePolicyConditionKey +): ResourcePolicyConditionDefinition { + return RESOURCE_POLICY_CONDITION_DEFINITIONS[key] +} + +export function requireResourcePolicyConditionDefinition( + key: string +): ResourcePolicyConditionDefinition { + if (!Object.hasOwn(RESOURCE_POLICY_CONDITION_DEFINITIONS, key)) { + throw new Error(`Resource policy condition key ${key} is not registered`) + } + return RESOURCE_POLICY_CONDITION_DEFINITIONS[key as ResourcePolicyConditionKey] +} diff --git a/apps/sim/lib/resource-policies/conditions/types.ts b/apps/sim/lib/resource-policies/conditions/types.ts new file mode 100644 index 00000000000..d0e834177c0 --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/types.ts @@ -0,0 +1,63 @@ +export const RESOURCE_POLICY_CONDITION_OPERATORS = ['Bool', 'StringEquals'] as const + +export type ResourcePolicyConditionOperator = (typeof RESOURCE_POLICY_CONDITION_OPERATORS)[number] + +export interface ResourcePolicyConditionEvaluationFacts { + credentialGroupActorEnrollmentId?: string + credentialGroupCredentialEnrollmentId?: string + currentWorkflow?: { + workflowId: string + mode: 'draft' | 'deployment' + } +} + +export interface ResourcePolicyConditionOption { + value: string | boolean + label: string +} + +export type ResourcePolicyConditionSelector = + | { type: 'static'; options: readonly ResourcePolicyConditionOption[] } + | { type: 'internal' } + +export type ResourcePolicyConditionValueType = 'boolean' | 'string' + +export interface ResourcePolicyConditionDefinition { + key: ResourcePolicyConditionKey + label: string + valueType: ResourcePolicyConditionValueType + operators: readonly ResourcePolicyConditionOperator[] + selector: ResourcePolicyConditionSelector + resolve(facts: ResourcePolicyConditionEvaluationFacts): boolean | string | undefined +} + +export type ResourcePolicyConditionKey = + | 'credential_group:ActorOwnsCredential' + | 'execution:WorkflowMode' + +export function defineResourcePolicyCondition( + definition: ResourcePolicyConditionDefinition +): ResourcePolicyConditionDefinition { + if (!definition.label.trim()) { + throw new Error(`Resource policy condition ${definition.key} requires a label`) + } + if (definition.operators.length === 0) { + throw new Error(`Resource policy condition ${definition.key} requires an operator`) + } + if (definition.selector.type === 'static' && definition.selector.options.length === 0) { + throw new Error(`Resource policy condition ${definition.key} requires selector options`) + } + const expectedValueType = definition.valueType === 'boolean' ? 'boolean' : 'string' + if ( + definition.selector.type === 'static' && + definition.selector.options.some((option) => typeof option.value !== expectedValueType) + ) { + throw new Error(`Resource policy condition ${definition.key} has invalid selector values`) + } + const allowedOperators: readonly ResourcePolicyConditionOperator[] = + definition.valueType === 'boolean' ? ['Bool'] : ['StringEquals'] + if (definition.operators.some((operator) => !allowedOperators.includes(operator))) { + throw new Error(`Resource policy condition ${definition.key} has an incompatible operator`) + } + return Object.freeze(definition) +} diff --git a/apps/sim/lib/resource-policies/conditions/workflow-mode.ts b/apps/sim/lib/resource-policies/conditions/workflow-mode.ts new file mode 100644 index 00000000000..d8a94122c82 --- /dev/null +++ b/apps/sim/lib/resource-policies/conditions/workflow-mode.ts @@ -0,0 +1,18 @@ +import { defineResourcePolicyCondition } from '@/lib/resource-policies/conditions/types' + +export const WORKFLOW_MODE_RESOURCE_POLICY_CONDITION_KEY = 'execution:WorkflowMode' as const + +export const workflowModeResourcePolicyConditionDefinition = defineResourcePolicyCondition({ + key: WORKFLOW_MODE_RESOURCE_POLICY_CONDITION_KEY, + label: 'Workflow mode', + valueType: 'string', + operators: ['StringEquals'], + selector: { + type: 'static', + options: [ + { value: 'draft', label: 'Draft' }, + { value: 'deployment', label: 'Deployed' }, + ], + }, + resolve: (facts) => facts.currentWorkflow?.mode, +}) diff --git a/apps/sim/lib/resource-policies/evaluator.test.ts b/apps/sim/lib/resource-policies/evaluator.test.ts new file mode 100644 index 00000000000..01a3e0ff7c0 --- /dev/null +++ b/apps/sim/lib/resource-policies/evaluator.test.ts @@ -0,0 +1,95 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { evaluateResourcePolicy } from '@/lib/resource-policies/evaluator' +import type { ResourcePolicyDocument, ResourcePolicyStatement } from '@/lib/resource-policies/types' + +const ALLOW: ResourcePolicyStatement = { + sid: 'AllowWorkflow', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { StringEquals: { 'execution:WorkflowMode': 'deployment' } }, +} + +const ACTOR_ALLOW: ResourcePolicyStatement = { + sid: 'AllowActorCredential', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'credential_group_actor' }], + condition: { Bool: { 'credential_group:ActorOwnsCredential': true } }, +} + +function document( + statements: readonly ResourcePolicyStatement[] +): ResourcePolicyDocument<'credential_group'> { + return { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements, + } +} + +describe('resource policy evaluator', () => { + it('matches registered principals and conditions', () => { + expect( + evaluateResourcePolicy({ + document: document([ALLOW]), + action: 'credential_groups.credentials.use', + facts: { currentWorkflow: { workflowId: 'workflow-1', mode: 'deployment' } }, + }) + ).toEqual({ decision: 'allow', statementSid: 'AllowWorkflow' }) + expect( + evaluateResourcePolicy({ + document: document([ALLOW]), + action: 'credential_groups.credentials.use', + facts: { currentWorkflow: { workflowId: 'workflow-1', mode: 'draft' } }, + }) + ).toEqual({ decision: 'implicit_deny' }) + }) + + it('matches a Credential Group actor only against their own credential', () => { + expect( + evaluateResourcePolicy({ + document: document([ACTOR_ALLOW]), + action: 'credential_groups.credentials.use', + facts: { + credentialGroupActorEnrollmentId: 'enrollment-1', + credentialGroupCredentialEnrollmentId: 'enrollment-1', + }, + }) + ).toEqual({ decision: 'allow', statementSid: 'AllowActorCredential' }) + expect( + evaluateResourcePolicy({ + document: document([ACTOR_ALLOW]), + action: 'credential_groups.credentials.use', + facts: { + credentialGroupActorEnrollmentId: 'enrollment-1', + credentialGroupCredentialEnrollmentId: 'enrollment-2', + }, + }) + ).toEqual({ decision: 'implicit_deny' }) + }) + + it('gives matching denies precedence over allows', () => { + expect( + evaluateResourcePolicy({ + document: document([{ ...ALLOW, sid: 'DenyWorkflow', effect: 'deny' }, ALLOW]), + action: 'credential_groups.credentials.use', + facts: { currentWorkflow: { workflowId: 'workflow-1', mode: 'deployment' } }, + }) + ).toEqual({ decision: 'deny', statementSid: 'DenyWorkflow' }) + }) + + it('fails fast on an empty condition', () => { + expect(() => + evaluateResourcePolicy({ + document: document([{ ...ALLOW, condition: {} }]), + action: 'credential_groups.credentials.use', + facts: { currentWorkflow: { workflowId: 'workflow-1', mode: 'deployment' } }, + }) + ).toThrow('condition must not be empty') + }) +}) diff --git a/apps/sim/lib/resource-policies/evaluator.ts b/apps/sim/lib/resource-policies/evaluator.ts new file mode 100644 index 00000000000..bb33f202fbd --- /dev/null +++ b/apps/sim/lib/resource-policies/evaluator.ts @@ -0,0 +1,111 @@ +import { + type ResourcePolicyConditionEvaluationFacts, + type ResourcePolicyConditionOperator, + requireResourcePolicyConditionDefinition, +} from '@/lib/resource-policies/conditions' +import { + matchResourcePolicyPrincipal, + type ResourcePolicyPrincipalEvaluationFacts, +} from '@/lib/resource-policies/principals' +import { getResourcePolicyDefinition } from '@/lib/resource-policies/registry' +import type { + ResourcePolicyAction, + ResourcePolicyDocument, + ResourcePolicyResourceType, + ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +export type ResourcePolicyDecision = + | { decision: 'allow'; statementSid: string } + | { decision: 'deny'; statementSid: string } + | { decision: 'implicit_deny' } + +export interface ResourcePolicyEvaluationFacts + extends ResourcePolicyPrincipalEvaluationFacts, + ResourcePolicyConditionEvaluationFacts {} + +interface EvaluateResourcePolicyInput { + document: ResourcePolicyDocument + action: ResourcePolicyAction + facts: ResourcePolicyEvaluationFacts +} + +function conditionMatches( + statement: ResourcePolicyStatement, + definition: ReturnType, + facts: ResourcePolicyConditionEvaluationFacts +): boolean { + if (!statement.condition) return true + const operators = Object.entries(statement.condition) + if (operators.length === 0) throw new Error('Resource policy condition must not be empty') + + for (const [operator, entries] of operators) { + if (!entries || Object.keys(entries).length === 0) { + throw new Error(`Resource policy condition operator ${operator} must not be empty`) + } + for (const [key, expected] of Object.entries(entries)) { + const condition = requireResourcePolicyConditionDefinition(key) + if (!definition.conditionKeys.includes(condition.key)) { + throw new Error(`Condition key ${key} does not apply to this resource`) + } + if (!condition.operators.includes(operator as ResourcePolicyConditionOperator)) { + throw new Error(`Condition operator ${operator} does not apply to ${key}`) + } + const expectedType = condition.valueType === 'boolean' ? 'boolean' : 'string' + if (typeof expected !== expectedType) { + throw new Error(`Condition ${key} requires a ${expectedType} value`) + } + if (condition.resolve(facts) !== expected) return false + } + } + return true +} + +function statementMatches( + statement: ResourcePolicyStatement, + input: EvaluateResourcePolicyInput +): boolean { + const definition = getResourcePolicyDefinition(input.document.resource.type) + for (const action of statement.actions) { + if (!definition.actions.includes(action)) { + throw new Error(`Action ${action} does not apply to ${input.document.resource.type}`) + } + } + if (!statement.actions.includes(input.action)) return false + if (statement.principals.length === 0) { + throw new Error(`Resource policy statement ${statement.sid} must contain a principal`) + } + const principalMatches = statement.principals.some((principal) => { + if (!definition.principalTypes.includes(principal.type)) { + throw new Error(`Principal ${principal.type} does not apply to this resource`) + } + return matchResourcePolicyPrincipal(principal, input.facts) + }) + return principalMatches && conditionMatches(statement, definition, input.facts) +} + +export function evaluateResourcePolicy( + input: EvaluateResourcePolicyInput +): ResourcePolicyDecision { + const definition = getResourcePolicyDefinition(input.document.resource.type) + if (!definition.actions.includes(input.action)) { + throw new Error(`Action ${input.action} does not apply to ${input.document.resource.type}`) + } + for (const statement of input.document.statements) { + if (statement.effect !== 'allow' && statement.effect !== 'deny') { + throw new Error(`Resource policy statement ${statement.sid} has an invalid effect`) + } + } + + for (const statement of input.document.statements) { + if (statement.effect === 'deny' && statementMatches(statement, input)) { + return { decision: 'deny', statementSid: statement.sid } + } + } + for (const statement of input.document.statements) { + if (statement.effect === 'allow' && statementMatches(statement, input)) { + return { decision: 'allow', statementSid: statement.sid } + } + } + return { decision: 'implicit_deny' } +} diff --git a/apps/sim/lib/resource-policies/principals/credential-group-actor.ts b/apps/sim/lib/resource-policies/principals/credential-group-actor.ts new file mode 100644 index 00000000000..cf6ba6195be --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/credential-group-actor.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' +import { defineResourcePolicyPrincipal } from '@/lib/resource-policies/principals/types' + +export const credentialGroupActorResourcePolicyPrincipalSchema = z + .object({ type: z.literal('credential_group_actor') }) + .strict() + +export const credentialGroupActorResourcePolicyPrincipalDefinition = defineResourcePolicyPrincipal({ + type: 'credential_group_actor', + schema: credentialGroupActorResourcePolicyPrincipalSchema, + label: 'Credential Group actor', + selector: { type: 'internal' }, + matches: (_principal, facts) => facts.credentialGroupActorEnrollmentId !== undefined, +}) diff --git a/apps/sim/lib/resource-policies/principals/index.ts b/apps/sim/lib/resource-policies/principals/index.ts new file mode 100644 index 00000000000..303918f1513 --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/index.ts @@ -0,0 +1,17 @@ +export { credentialGroupActorResourcePolicyPrincipalSchema } from '@/lib/resource-policies/principals/credential-group-actor' +export { + getResourcePolicyPrincipalDefinition, + matchResourcePolicyPrincipal, + RESOURCE_POLICY_PRINCIPAL_DEFINITIONS, + requireResourcePolicyPrincipalDefinition, +} from '@/lib/resource-policies/principals/registry' +export type { + CredentialGroupActorResourcePolicyPrincipal, + ResourcePolicyPrincipal, + ResourcePolicyPrincipalDefinition, + ResourcePolicyPrincipalEvaluationFacts, + ResourcePolicyPrincipalSelector, + ResourcePolicyPrincipalType, + WorkflowResourcePolicyPrincipal, +} from '@/lib/resource-policies/principals/types' +export { workflowResourcePolicyPrincipalSchema } from '@/lib/resource-policies/principals/workflow' diff --git a/apps/sim/lib/resource-policies/principals/registry.test.ts b/apps/sim/lib/resource-policies/principals/registry.test.ts new file mode 100644 index 00000000000..bb98a8de73f --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/registry.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { + matchResourcePolicyPrincipal, + RESOURCE_POLICY_PRINCIPAL_DEFINITIONS, + requireResourcePolicyPrincipalDefinition, +} from '@/lib/resource-policies/principals' + +describe('resource policy principal registry', () => { + it('registers the internal Credential Group actor principal', () => { + expect(RESOURCE_POLICY_PRINCIPAL_DEFINITIONS.credential_group_actor.selector).toEqual({ + type: 'internal', + }) + expect( + matchResourcePolicyPrincipal( + { type: 'credential_group_actor' }, + { credentialGroupActorEnrollmentId: 'enrollment-1' } + ) + ).toBe(true) + expect(matchResourcePolicyPrincipal({ type: 'credential_group_actor' }, {})).toBe(false) + }) + + it('registers workflow matching and its catalog selector together', () => { + expect(RESOURCE_POLICY_PRINCIPAL_DEFINITIONS.workflow.selector).toEqual({ + type: 'catalog', + catalog: 'workflows', + }) + expect( + matchResourcePolicyPrincipal( + { type: 'workflow', workflowId: 'workflow-1' }, + { currentWorkflow: { workflowId: 'workflow-1', mode: 'deployment' } } + ) + ).toBe(true) + expect( + matchResourcePolicyPrincipal( + { type: 'workflow', workflowId: 'workflow-1' }, + { currentWorkflow: { workflowId: 'workflow-2', mode: 'deployment' } } + ) + ).toBe(false) + }) + + it('fails fast for an unregistered principal type', () => { + expect(() => requireResourcePolicyPrincipalDefinition('user')).toThrow( + 'Resource policy principal type user is not registered' + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/principals/registry.ts b/apps/sim/lib/resource-policies/principals/registry.ts new file mode 100644 index 00000000000..9649267f5f8 --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/registry.ts @@ -0,0 +1,35 @@ +import { credentialGroupActorResourcePolicyPrincipalDefinition } from '@/lib/resource-policies/principals/credential-group-actor' +import type { + ResourcePolicyPrincipal, + ResourcePolicyPrincipalDefinition, + ResourcePolicyPrincipalEvaluationFacts, + ResourcePolicyPrincipalType, +} from '@/lib/resource-policies/principals/types' +import { workflowResourcePolicyPrincipalDefinition } from '@/lib/resource-policies/principals/workflow' + +export const RESOURCE_POLICY_PRINCIPAL_DEFINITIONS = Object.freeze({ + credential_group_actor: credentialGroupActorResourcePolicyPrincipalDefinition, + workflow: workflowResourcePolicyPrincipalDefinition, +} as const satisfies Record) + +export function getResourcePolicyPrincipalDefinition( + type: ResourcePolicyPrincipalType +): ResourcePolicyPrincipalDefinition { + return RESOURCE_POLICY_PRINCIPAL_DEFINITIONS[type] +} + +export function requireResourcePolicyPrincipalDefinition( + type: string +): ResourcePolicyPrincipalDefinition { + if (!Object.hasOwn(RESOURCE_POLICY_PRINCIPAL_DEFINITIONS, type)) { + throw new Error(`Resource policy principal type ${type} is not registered`) + } + return RESOURCE_POLICY_PRINCIPAL_DEFINITIONS[type as ResourcePolicyPrincipalType] +} + +export function matchResourcePolicyPrincipal( + principal: ResourcePolicyPrincipal, + facts: ResourcePolicyPrincipalEvaluationFacts +): boolean { + return getResourcePolicyPrincipalDefinition(principal.type).matches(principal, facts) +} diff --git a/apps/sim/lib/resource-policies/principals/types.ts b/apps/sim/lib/resource-policies/principals/types.ts new file mode 100644 index 00000000000..1df07d52033 --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/types.ts @@ -0,0 +1,46 @@ +import type { ZodType } from 'zod' + +export interface WorkflowResourcePolicyPrincipal { + type: 'workflow' + workflowId: string +} + +export interface CredentialGroupActorResourcePolicyPrincipal { + type: 'credential_group_actor' +} + +export type ResourcePolicyPrincipal = + | WorkflowResourcePolicyPrincipal + | CredentialGroupActorResourcePolicyPrincipal +export type ResourcePolicyPrincipalType = ResourcePolicyPrincipal['type'] + +export interface ResourcePolicyPrincipalEvaluationFacts { + credentialGroupActorEnrollmentId?: string + currentWorkflow?: { + workflowId: string + mode: 'draft' | 'deployment' + } +} + +export type ResourcePolicyPrincipalSelector = + | { type: 'catalog'; catalog: 'workflows' } + | { type: 'internal' } + +export interface ResourcePolicyPrincipalDefinition< + Principal extends ResourcePolicyPrincipal = ResourcePolicyPrincipal, +> { + type: Principal['type'] + schema: ZodType + label: string + selector: ResourcePolicyPrincipalSelector + matches(principal: Principal, facts: ResourcePolicyPrincipalEvaluationFacts): boolean +} + +export function defineResourcePolicyPrincipal( + definition: ResourcePolicyPrincipalDefinition +): ResourcePolicyPrincipalDefinition { + if (!definition.label.trim()) { + throw new Error(`Resource policy principal ${definition.type} requires a label`) + } + return Object.freeze(definition) +} diff --git a/apps/sim/lib/resource-policies/principals/workflow.ts b/apps/sim/lib/resource-policies/principals/workflow.ts new file mode 100644 index 00000000000..3577f87dd42 --- /dev/null +++ b/apps/sim/lib/resource-policies/principals/workflow.ts @@ -0,0 +1,21 @@ +import { z } from 'zod' +import { defineResourcePolicyPrincipal } from '@/lib/resource-policies/principals/types' + +export const workflowResourcePolicyPrincipalSchema = z + .object({ + type: z.literal('workflow'), + workflowId: z + .string() + .min(1) + .max(128) + .refine((value) => value === value.trim(), 'Workflow ID must be canonical'), + }) + .strict() + +export const workflowResourcePolicyPrincipalDefinition = defineResourcePolicyPrincipal({ + type: 'workflow', + schema: workflowResourcePolicyPrincipalSchema, + label: 'Workflow', + selector: { type: 'catalog', catalog: 'workflows' }, + matches: (principal, facts) => facts.currentWorkflow?.workflowId === principal.workflowId, +}) diff --git a/apps/sim/lib/resource-policies/registry.ts b/apps/sim/lib/resource-policies/registry.ts new file mode 100644 index 00000000000..c59bee2fd1a --- /dev/null +++ b/apps/sim/lib/resource-policies/registry.ts @@ -0,0 +1,57 @@ +import type { ResourcePolicyConditionKey } from '@/lib/resource-policies/conditions' +import type { ResourcePolicyPrincipalType } from '@/lib/resource-policies/principals' + +export const RESOURCE_POLICY_RESOURCE_TYPES = ['credential_group'] as const +export const CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION = 'credential_groups.credentials.use' as const +export const RESOURCE_POLICY_ACTIONS = [CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION] as const + +export type ResourcePolicyResourceType = (typeof RESOURCE_POLICY_RESOURCE_TYPES)[number] +export type ResourcePolicyAction = (typeof RESOURCE_POLICY_ACTIONS)[number] + +interface ResourcePolicyResourceDefinition { + actions: readonly ResourcePolicyAction[] + principalTypes: readonly ResourcePolicyPrincipalType[] + conditionKeys: readonly ResourcePolicyConditionKey[] +} + +export const RESOURCE_POLICY_DEFINITIONS = Object.freeze({ + credential_group: { + actions: RESOURCE_POLICY_ACTIONS, + principalTypes: ['credential_group_actor', 'workflow'], + conditionKeys: ['credential_group:ActorOwnsCredential', 'execution:WorkflowMode'], + }, +} as const satisfies Record) + +type ResourcePolicyDefinitionMap = typeof RESOURCE_POLICY_DEFINITIONS + +export type ResourcePolicyBinding = { + [ResourceType in ResourcePolicyResourceType]: { + readonly resourceType: ResourceType + readonly action: ResourcePolicyDefinitionMap[ResourceType]['actions'][number] + } +}[ResourcePolicyResourceType] + +export type ResourcePolicyBindingFor = Extract< + ResourcePolicyBinding, + { readonly resourceType: ResourceType } +> + +export function getResourcePolicyDefinition( + resourceType: ResourcePolicyResourceType +): ResourcePolicyResourceDefinition { + return RESOURCE_POLICY_DEFINITIONS[resourceType] +} + +export function requireResourcePolicyBinding(binding: ResourcePolicyBinding): void { + if ( + !RESOURCE_POLICY_RESOURCE_TYPES.some((resourceType) => resourceType === binding.resourceType) + ) { + throw new Error(`Unknown resource policy resource type: ${binding.resourceType}`) + } + const definition = getResourcePolicyDefinition(binding.resourceType) + if (!definition.actions.includes(binding.action)) { + throw new Error( + `Action ${binding.action} does not apply to resource policy type ${binding.resourceType}` + ) + } +} diff --git a/apps/sim/lib/resource-policies/repository.test.ts b/apps/sim/lib/resource-policies/repository.test.ts new file mode 100644 index 00000000000..3fb3e222320 --- /dev/null +++ b/apps/sim/lib/resource-policies/repository.test.ts @@ -0,0 +1,132 @@ +/** + * @vitest-environment node + */ +import { + dbChainMock, + dbChainMockFns, + hasMockCondition, + queueTableRows, + resetDbChainMock, + schemaMock, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + compileCredentialGroupWorkflowAccessPolicy, + credentialGroupWorkflowAccessPolicyCodec, +} from '@/lib/credential-groups/application/workflow-access-policy' +import { + deleteResourcePolicyForResource, + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, + requireResourcePolicy, + writeResourcePolicy, +} from '@/lib/resource-policies/repository' + +const TARGET = { + workspaceId: 'workspace-1', + resourceType: 'credential_group' as const, + resourceId: 'group-1', + codec: credentialGroupWorkflowAccessPolicyCodec, +} +const DEFAULT_DOCUMENT = compileCredentialGroupWorkflowAccessPolicy({ + credentialGroupId: TARGET.resourceId, + allowedWorkflowIds: [], +}) + +function storedRow(revision = 1, document: unknown = DEFAULT_DOCUMENT) { + return { + id: 'policy-1', + workspaceId: TARGET.workspaceId, + resourceType: TARGET.resourceType, + resourceId: TARGET.resourceId, + revision, + document, + createdBy: 'admin-1', + updatedBy: 'admin-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } +} + +describe('resource policy repository', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('requires the canonical workspace, resource type, and resource ID', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow()]) + + await expect(requireResourcePolicy(TARGET)).resolves.toMatchObject({ + id: 'policy-1', + workspaceId: TARGET.workspaceId, + revision: 1, + document: DEFAULT_DOCUMENT, + }) + + const where = dbChainMockFns.where.mock.calls.at(-1)?.[0] + for (const expected of [TARGET.workspaceId, TARGET.resourceType, TARGET.resourceId]) { + expect( + hasMockCondition( + where, + (condition) => condition.type === 'eq' && condition.right === expected + ) + ).toBe(true) + } + }) + + it('fails fast for a missing or malformed stored policy', async () => { + await expect(requireResourcePolicy(TARGET)).rejects.toBeInstanceOf(ResourcePolicyNotFoundError) + + queueTableRows(schemaMock.resourcePolicy, [storedRow(1, { version: 1 })]) + await expect(requireResourcePolicy(TARGET)).rejects.toThrow() + }) + + it('locks and advances exactly the expected revision', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow(3)]) + const updated = { ...storedRow(4), updatedAt: new Date('2026-08-20T01:00:00.000Z') } + dbChainMockFns.returning.mockResolvedValueOnce([updated]) + + await expect( + writeResourcePolicy({ + ...TARGET, + expectedRevision: 3, + actorUserId: 'admin-2', + document: DEFAULT_DOCUMENT, + }) + ).resolves.toMatchObject({ revision: 4, document: DEFAULT_DOCUMENT }) + + expect(dbChainMockFns.for).toHaveBeenCalledWith('update') + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ revision: 4, updatedBy: 'admin-2' }) + ) + const updateWhere = dbChainMockFns.where.mock.calls.at(-1)?.[0] + expect( + hasMockCondition(updateWhere, (condition) => condition.type === 'eq' && condition.right === 3) + ).toBe(true) + }) + + it('rejects a stale revision without issuing an update', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow(4)]) + + await expect( + writeResourcePolicy({ + ...TARGET, + expectedRevision: 3, + actorUserId: 'admin-2', + document: DEFAULT_DOCUMENT, + }) + ).rejects.toBeInstanceOf(ResourcePolicyRevisionConflictError) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + + it('requires exactly one policy row when deleting a resource', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'policy-1' }]) + await expect(deleteResourcePolicyForResource(TARGET, dbChainMock.db)).resolves.toBeUndefined() + + dbChainMockFns.returning.mockResolvedValueOnce([]) + await expect(deleteResourcePolicyForResource(TARGET, dbChainMock.db)).rejects.toBeInstanceOf( + ResourcePolicyNotFoundError + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/repository.ts b/apps/sim/lib/resource-policies/repository.ts new file mode 100644 index 00000000000..511edcf8368 --- /dev/null +++ b/apps/sim/lib/resource-policies/repository.ts @@ -0,0 +1,169 @@ +import { db } from '@sim/db' +import { resourcePolicy } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +import type { + ResourcePolicyCodec, + ResourcePolicyDocument, + ResourcePolicyResourceType, + ResourcePolicyTarget, +} from '@/lib/resource-policies/types' + +export interface StoredResourcePolicy< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +> { + id: string + workspaceId: string + revision: number + document: Document + createdAt: Date + updatedAt: Date +} + +export class ResourcePolicyNotFoundError extends Error { + constructor(resourceType: ResourcePolicyResourceType, resourceId: string) { + super(`Required resource policy is missing for ${resourceType} ${resourceId}`) + this.name = 'ResourcePolicyNotFoundError' + } +} + +export class ResourcePolicyRevisionConflictError extends Error { + constructor() { + super('Resource policy changed while it was being edited') + this.name = 'ResourcePolicyRevisionConflictError' + } +} + +type CodecTarget< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +> = ResourcePolicyTarget & { + codec: ResourcePolicyCodec +} + +function requireMatchingCodec( + resourceType: ResourceType, + codec: { readonly resourceType: ResourcePolicyResourceType } +): void { + if (codec.resourceType !== resourceType) { + throw new Error( + `Resource policy codec ${codec.resourceType} cannot parse resource type ${resourceType}` + ) + } +} + +async function loadResourcePolicyWithExecutor< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +>( + input: CodecTarget, + executor: DbOrTx, + options: { forUpdate?: boolean } = {} +): Promise | null> { + requireMatchingCodec(input.resourceType, input.codec) + const query = executor + .select() + .from(resourcePolicy) + .where( + and( + eq(resourcePolicy.workspaceId, input.workspaceId), + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId) + ) + ) + .limit(1) + const rows = options.forUpdate ? await query.for('update') : await query + const row = rows[0] + if (!row) return null + return { + id: row.id, + workspaceId: row.workspaceId, + revision: row.revision, + document: input.codec.parse(row.document, { + type: input.resourceType, + id: input.resourceId, + }), + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } +} + +export async function requireResourcePolicy< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +>( + input: CodecTarget, + executor: DbOrTx = db +): Promise> { + const policy = await loadResourcePolicyWithExecutor(input, executor) + if (!policy) throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) + return policy +} + +export async function writeResourcePolicy< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +>( + input: CodecTarget & { + expectedRevision: number + document: Document + actorUserId: string + } +): Promise> { + if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) { + throw new Error('Expected resource policy revision must be a positive integer') + } + const document = input.codec.parse(input.document, { + type: input.resourceType, + id: input.resourceId, + }) + + return db.transaction(async (tx) => { + const existing = await loadResourcePolicyWithExecutor(input, tx, { forUpdate: true }) + if (!existing) throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) + if (existing.revision !== input.expectedRevision) { + throw new ResourcePolicyRevisionConflictError() + } + + const [updated] = await tx + .update(resourcePolicy) + .set({ + document, + revision: input.expectedRevision + 1, + updatedBy: input.actorUserId, + updatedAt: new Date(), + }) + .where( + and(eq(resourcePolicy.id, existing.id), eq(resourcePolicy.revision, input.expectedRevision)) + ) + .returning() + if (!updated) throw new Error('Locked resource policy update returned no row') + return { + id: updated.id, + workspaceId: updated.workspaceId, + revision: updated.revision, + document, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + } + }) +} + +export async function deleteResourcePolicyForResource< + ResourceType extends ResourcePolicyResourceType, +>(input: ResourcePolicyTarget, executor: DbOrTx): Promise { + const deleted = await executor + .delete(resourcePolicy) + .where( + and( + eq(resourcePolicy.workspaceId, input.workspaceId), + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId) + ) + ) + .returning({ id: resourcePolicy.id }) + if (deleted.length !== 1) { + throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) + } +} diff --git a/apps/sim/lib/resource-policies/types.ts b/apps/sim/lib/resource-policies/types.ts new file mode 100644 index 00000000000..7daed5be6ae --- /dev/null +++ b/apps/sim/lib/resource-policies/types.ts @@ -0,0 +1,54 @@ +import type { + ResourcePolicyConditionKey, + ResourcePolicyConditionOperator, +} from '@/lib/resource-policies/conditions' +import type { ResourcePolicyPrincipal } from '@/lib/resource-policies/principals' +import type { + ResourcePolicyAction, + ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' + +export { + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_RESOURCE_TYPES, + type ResourcePolicyAction, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' + +export type ResourcePolicyEffect = 'allow' | 'deny' + +export type ResourcePolicyCondition = Partial< + Record>> +> + +export interface ResourcePolicyStatement { + sid: string + effect: ResourcePolicyEffect + actions: readonly ResourcePolicyAction[] + principals: readonly ResourcePolicyPrincipal[] + condition?: ResourcePolicyCondition +} + +export interface ResourcePolicyTarget { + workspaceId: string + resourceType: ResourceType + resourceId: string +} + +export interface ResourcePolicyDocument { + version: number + resource: { + type: ResourceType + id: string + } + statements: readonly ResourcePolicyStatement[] +} + +/** Lets each resource own its strict document format while storage stays resource-agnostic. */ +export interface ResourcePolicyCodec< + ResourceType extends ResourcePolicyResourceType, + Document extends ResourcePolicyDocument, +> { + readonly resourceType: ResourceType + parse(value: unknown, expected: { type: ResourceType; id: string }): Document +} diff --git a/apps/sim/lib/workflows/application/context.test.ts b/apps/sim/lib/workflows/application/context.test.ts index 8a31c794da3..68c9c5f1f10 100644 --- a/apps/sim/lib/workflows/application/context.test.ts +++ b/apps/sim/lib/workflows/application/context.test.ts @@ -1,7 +1,13 @@ /** * @vitest-environment node */ -import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { + dbChainMockFns, + flattenMockConditions, + queueTableRows, + resetDbChainMock, + schemaMock, +} from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ @@ -17,6 +23,8 @@ vi.mock('@/lib/workspaces/application/workspace-context', () => ({ import { resolveActiveWorkflowApplicationContext, + resolveActiveWorkflowDeploymentVersionApplicationContext, + resolveActiveWorkflowExecutionApplicationContext, resolveActiveWorkflowRunApplicationContext, } from '@/lib/workflows/application/context' @@ -135,4 +143,141 @@ describe('workflow application contexts', () => { workspaceId: 'workspace-1', }) }) + + it('binds live execution authority to the deployment version stored on its durable log', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, [ + { + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + deploymentVersionId: 'deployment-version-old', + }, + ]) + queueTableRows(schemaMock.resumeQueue, []) + queueTableRows(schemaMock.workflow, [ + { + workflowId: 'workflow-1', + workflow, + workspaceId: 'workspace-1', + }, + ]) + + await expect( + resolveActiveWorkflowExecutionApplicationContext({ + runId: 'run-1', + assertedWorkflowId: 'workflow-1', + }) + ).resolves.toMatchObject({ + runId: 'run-1', + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + deploymentVersionId: 'deployment-version-old', + }) + + const conditions = flattenMockConditions(dbChainMockFns.where.mock.calls[0]?.[0]) + expect(conditions).toContainEqual({ + type: 'inArray', + column: 'workflowExecutionLogs.status', + values: ['running', 'pending', 'paused'], + }) + }) + + it('rejects execution authority without a live durable log', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, []) + queueTableRows(schemaMock.resumeQueue, []) + + await expect( + resolveActiveWorkflowExecutionApplicationContext({ + runId: 'run-1', + assertedWorkflowId: 'workflow-1', + }) + ).rejects.toMatchObject({ code: 'not_found', message: 'Run not found' }) + expect(mocks.loadWorkspace).not.toHaveBeenCalled() + }) + + it('binds a claimed resume attempt to its parent durable execution log', async () => { + queueTableRows(schemaMock.workflowExecutionLogs, []) + queueTableRows(schemaMock.resumeQueue, [ + { + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + deploymentVersionId: 'deployment-version-old', + }, + ]) + queueTableRows(schemaMock.workflow, [ + { + workflowId: 'workflow-1', + workflow, + workspaceId: 'workspace-1', + }, + ]) + + await expect( + resolveActiveWorkflowExecutionApplicationContext({ + runId: 'resume-execution-1', + assertedWorkflowId: 'workflow-1', + }) + ).resolves.toMatchObject({ + runId: 'resume-execution-1', + deploymentVersionId: 'deployment-version-old', + }) + + const resumeConditions = flattenMockConditions(dbChainMockFns.where.mock.calls[1]?.[0]) + expect(resumeConditions).toContainEqual({ + type: 'eq', + left: 'resumeQueue.newExecutionId', + right: 'resume-execution-1', + }) + expect(resumeConditions).toContainEqual({ + type: 'eq', + left: 'resumeQueue.status', + right: 'claimed', + }) + }) + + it('accepts an exact historical deployment version without requiring it to be active', async () => { + queueTableRows(schemaMock.workflow, [ + { + workflowId: 'workflow-1', + workflow, + workspaceId: 'workspace-1', + }, + ]) + queueTableRows(schemaMock.workflowDeploymentVersion, [ + { deploymentVersionId: 'deployment-version-old' }, + ]) + + await expect( + resolveActiveWorkflowDeploymentVersionApplicationContext({ + workflowId: 'workflow-1', + deploymentVersionId: 'deployment-version-old', + assertedWorkspaceId: 'workspace-1', + }) + ).resolves.toMatchObject({ + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + deploymentVersionId: 'deployment-version-old', + }) + }) + + it('rejects a deployment version that does not belong to the claimed workflow', async () => { + queueTableRows(schemaMock.workflow, [ + { + workflowId: 'workflow-1', + workflow, + workspaceId: 'workspace-1', + }, + ]) + queueTableRows(schemaMock.workflowDeploymentVersion, []) + + await expect( + resolveActiveWorkflowDeploymentVersionApplicationContext({ + workflowId: 'workflow-1', + deploymentVersionId: 'deployment-version-forged', + assertedWorkspaceId: 'workspace-1', + }) + ).rejects.toMatchObject({ + code: 'not_found', + message: 'Workflow deployment version not found', + }) + }) }) diff --git a/apps/sim/lib/workflows/application/context.ts b/apps/sim/lib/workflows/application/context.ts index 1d4f928cce7..ade9755b86b 100644 --- a/apps/sim/lib/workflows/application/context.ts +++ b/apps/sim/lib/workflows/application/context.ts @@ -1,6 +1,12 @@ import { db } from '@sim/db' -import { pausedExecutions, resumeQueue, workflow, workflowExecutionLogs } from '@sim/db/schema' -import { and, eq, isNull } from 'drizzle-orm' +import { + pausedExecutions, + resumeQueue, + workflow, + workflowDeploymentVersion, + workflowExecutionLogs, +} from '@sim/db/schema' +import { and, eq, inArray, isNull } from 'drizzle-orm' import { getJobQueue } from '@/lib/core/async-jobs' import { OrchestrationError } from '@/lib/core/orchestration/types' import { WORKFLOW_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/execution-job-ids' @@ -19,6 +25,16 @@ export interface ActiveWorkflowRunApplicationContext extends ActiveWorkflowAppli runId: string } +export interface ActiveWorkflowExecutionApplicationContext + extends ActiveWorkflowRunApplicationContext { + deploymentVersionId: string | null +} + +export interface ActiveWorkflowDeploymentVersionApplicationContext + extends ActiveWorkflowApplicationContext { + deploymentVersionId: string +} + export async function resolveActiveWorkflowApplicationContext(input: { workflowId: string assertedWorkspaceId?: string @@ -134,3 +150,98 @@ export async function resolveActiveWorkflowRunApplicationContext(input: { }) return { ...context, runId: input.runId } } + +/** Resolves an execution that is currently running or waiting to resume from its durable log. */ +export async function resolveActiveWorkflowExecutionApplicationContext(input: { + runId: string + assertedWorkflowId?: string +}): Promise { + const projection = { + workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, + deploymentVersionId: workflowExecutionLogs.deploymentVersionId, + } + const [directRows, resumedRows] = await Promise.all([ + db + .select(projection) + .from(workflowExecutionLogs) + .where( + and( + eq(workflowExecutionLogs.executionId, input.runId), + inArray(workflowExecutionLogs.status, ['running', 'pending', 'paused']) + ) + ) + .limit(1), + db + .select(projection) + .from(resumeQueue) + .innerJoin( + workflowExecutionLogs, + eq(resumeQueue.parentExecutionId, workflowExecutionLogs.executionId) + ) + .where( + and( + eq(resumeQueue.newExecutionId, input.runId), + eq(resumeQueue.status, 'claimed'), + inArray(workflowExecutionLogs.status, ['running', 'pending', 'paused']) + ) + ) + .limit(1), + ]) + const direct = directRows[0] + const resumed = resumedRows[0] + if ( + direct && + resumed && + (resumed.workflowId !== direct.workflowId || + resumed.workspaceId !== direct.workspaceId || + resumed.deploymentVersionId !== direct.deploymentVersionId) + ) { + throw new Error(`Execution ${input.runId} has conflicting durable authority bindings`) + } + const run = direct ?? resumed + + if ( + !run?.workflowId || + (input.assertedWorkflowId !== undefined && input.assertedWorkflowId !== run.workflowId) + ) { + throw new OrchestrationError('not_found', 'Run not found') + } + + const context = await resolveActiveWorkflowApplicationContext({ + workflowId: run.workflowId, + assertedWorkspaceId: run.workspaceId, + }) + return { + ...context, + runId: input.runId, + deploymentVersionId: run.deploymentVersionId, + } +} + +/** Resolves an immutable deployment version without requiring it to remain active. */ +export async function resolveActiveWorkflowDeploymentVersionApplicationContext(input: { + workflowId: string + deploymentVersionId: string + assertedWorkspaceId: string +}): Promise { + const context = await resolveActiveWorkflowApplicationContext({ + workflowId: input.workflowId, + assertedWorkspaceId: input.assertedWorkspaceId, + }) + const [version] = await db + .select({ deploymentVersionId: workflowDeploymentVersion.id }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.id, input.deploymentVersionId), + eq(workflowDeploymentVersion.workflowId, context.workflowId) + ) + ) + .limit(1) + + if (!version) { + throw new OrchestrationError('not_found', 'Workflow deployment version not found') + } + return { ...context, deploymentVersionId: version.deploymentVersionId } +} diff --git a/apps/sim/lib/workflows/executor/execution-core.test.ts b/apps/sim/lib/workflows/executor/execution-core.test.ts index 49f0f8a3dd1..20b2d2eee03 100644 --- a/apps/sim/lib/workflows/executor/execution-core.test.ts +++ b/apps/sim/lib/workflows/executor/execution-core.test.ts @@ -62,6 +62,8 @@ afterAll(resetEnvironmentUtilsMock) const loadWorkflowFromNormalizedTablesMock = workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables const loadDeployedWorkflowStateMock = workflowsPersistenceUtilsMockFns.mockLoadDeployedWorkflowState +const loadWorkflowDeploymentVersionStateMock = + workflowsPersistenceUtilsMockFns.mockLoadWorkflowDeploymentVersionState const updateWorkflowRunCountsMock = workflowsUtilsMockFns.mockUpdateWorkflowRunCounts vi.mock('@/lib/execution/cancellation', () => ({ @@ -202,6 +204,13 @@ describe('executeWorkflowCore terminal finalization sequencing', () => { parallels: {}, deploymentVersionId: 'dep-1', }) + loadWorkflowDeploymentVersionStateMock.mockResolvedValue({ + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'dep-historical', + }) getPersonalAndWorkspaceEnvMock.mockResolvedValue({ personalEncrypted: {}, @@ -781,6 +790,106 @@ describe('executeWorkflowCore terminal finalization sequencing', () => { expect(registry.getActiveMatches()).toEqual([]) }) + it('resumes a deployed run from its admitted historical version after deployment changes', async () => { + executorExecuteMock.mockResolvedValue({ + success: true, + status: 'completed', + output: { done: true }, + logs: [], + metadata: { duration: 1, startTime: 'start', endTime: 'end' }, + }) + const resumedSnapshot = createSnapshot() + resumedSnapshot.metadata = { + ...resumedSnapshot.metadata, + executionId: 'execution-resumed', + useDraftState: false, + resumeFromSnapshot: true, + resumeTerminalNoop: true, + workflowStateOverride: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'dep-active-now', + }, + } as any + ;(resumedSnapshot as any).state = { + blockStates: {}, + executedBlocks: [], + blockLogs: [], + decisions: { router: {}, condition: {} }, + completedLoops: [], + activeExecutionPath: [], + } + + await executeWorkflowCore({ + snapshot: resumedSnapshot as any, + callbacks: {}, + loggingSession: loggingSession as any, + skipLogCreation: true, + resumeDeploymentVersionId: 'dep-historical', + }) + + expect(loadWorkflowDeploymentVersionStateMock).toHaveBeenCalledWith( + 'workflow-1', + 'dep-historical', + 'workspace-1' + ) + expect(loadDeployedWorkflowStateMock).not.toHaveBeenCalled() + expect(safeStartMock).toHaveBeenCalledWith( + expect.objectContaining({ deploymentVersionId: 'dep-historical' }) + ) + expect(executorConstructorMock.mock.calls[0]?.[0]?.contextExtensions).toMatchObject({ + executorDelegationOrigin: { + currentWorkflow: { + workflowId: 'workflow-1', + mode: 'deployment', + deploymentVersionId: 'dep-historical', + }, + }, + }) + }) + + it('fails instead of loading the latest deployment for a deployed resume without authority', async () => { + const resumedSnapshot = createSnapshot() + resumedSnapshot.metadata = { + ...resumedSnapshot.metadata, + useDraftState: false, + resumeFromSnapshot: true, + } as any + + await expect( + executeWorkflowCore({ + snapshot: resumedSnapshot as any, + callbacks: {}, + loggingSession: loggingSession as any, + skipLogCreation: true, + }) + ).rejects.toThrow('Deployed resume requires its admitted deployment version') + expect(loadDeployedWorkflowStateMock).not.toHaveBeenCalled() + expect(loadWorkflowDeploymentVersionStateMock).not.toHaveBeenCalled() + }) + + it('rejects deployment authority on a draft resume', async () => { + const resumedSnapshot = createSnapshot() + resumedSnapshot.metadata = { + ...resumedSnapshot.metadata, + resumeFromSnapshot: true, + } as any + + await expect( + executeWorkflowCore({ + snapshot: resumedSnapshot as any, + callbacks: {}, + loggingSession: loggingSession as any, + skipLogCreation: true, + resumeDeploymentVersionId: 'dep-historical', + }) + ).rejects.toThrow('Draft resume cannot carry deployment version authority') + expect(loadWorkflowFromNormalizedTablesMock).not.toHaveBeenCalled() + expect(loadWorkflowDeploymentVersionStateMock).not.toHaveBeenCalled() + }) + it('marks inherited client run-from-block provenance incomplete', async () => { executorExecuteMock.mockResolvedValue({ success: true, diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 5aff8de7acd..f5ec36774a7 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -32,6 +32,7 @@ import { waitForChildRuns } from '@/lib/workflows/custom-blocks/child-execution' import { getCustomBlockRowsForWorkspace } from '@/lib/workflows/custom-blocks/operations' import { loadDeployedWorkflowState, + loadWorkflowDeploymentVersionState, loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' @@ -118,6 +119,8 @@ export interface ExecuteWorkflowCoreOptions { stopAfterBlockId?: string /** Trusted encrypted provenance captured by a server-only pre-execution boundary. */ trustedInitialResolvedSecretTraceProvenance?: ResolvedSecretTraceProvenanceV1 + /** Immutable deployment admitted by the durable parent log for a resumed execution. */ + resumeDeploymentVersionId?: string /** Run-from-block mode: execute starting from a specific block using cached upstream outputs */ runFromBlock?: { startBlockId: string @@ -395,6 +398,7 @@ async function executeWorkflowCoreImpl( base64MaxBytes, stopAfterBlockId, runFromBlock, + resumeDeploymentVersionId, } = options loggingSession.setExecutionDeadlineAt(getExecutionDeadlineAt(abortSignal)) const { metadata, input, workflowVariables, selectedOutputs } = snapshot @@ -406,6 +410,16 @@ async function executeWorkflowCoreImpl( if (!providedWorkspaceId) { throw new Error(`Execution metadata missing workspaceId for workflow ${workflowId}`) } + const resumeFromSnapshot = metadata.resumeFromSnapshot === true + if (!resumeFromSnapshot && resumeDeploymentVersionId !== undefined) { + throw new Error('Deployment version authority can only be supplied for a resumed execution') + } + if (resumeFromSnapshot && useDraftState && resumeDeploymentVersionId !== undefined) { + throw new Error('Draft resume cannot carry deployment version authority') + } + if (resumeFromSnapshot && !useDraftState && !resumeDeploymentVersionId) { + throw new Error('Deployed resume requires its admitted deployment version') + } let processedInput = input || {} let deploymentVersionId: string | undefined @@ -481,6 +495,25 @@ async function executeWorkflowCoreImpl( * on the environment load, so the two are awaited concurrently below. */ const loadWorkflowState = async () => { + if (resumeFromSnapshot && !useDraftState) { + if (!resumeDeploymentVersionId) { + throw new Error('Deployed resume requires its admitted deployment version') + } + const deployedData = await loadWorkflowDeploymentVersionState( + workflowId, + resumeDeploymentVersionId, + providedWorkspaceId + ) + logger.info(`[${requestId}] Using admitted historical deployment state (resumed execution)`) + return { + blocks: deployedData.blocks, + edges: deployedData.edges, + loops: deployedData.loops, + parallels: deployedData.parallels, + deploymentVersionId: deployedData.deploymentVersionId, + } + } + if (metadata.workflowStateOverride) { const override = metadata.workflowStateOverride logger.info(`[${requestId}] Using workflow state override (diff workflow execution)`, { @@ -515,7 +548,7 @@ async function executeWorkflowCoreImpl( } } - const deployedData = await loadDeployedWorkflowState(workflowId) + const deployedData = await loadDeployedWorkflowState(workflowId, providedWorkspaceId) logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`) return { blocks: deployedData.blocks, @@ -553,7 +586,6 @@ async function executeWorkflowCoreImpl( // Use already-decrypted values for execution (no redundant decryption) const decryptedEnvVars: Record = { ...personalDecrypted, ...workspaceDecrypted } - const resumeFromSnapshot = metadata.resumeFromSnapshot === true const restoredState = runFromBlock?.sourceSnapshot ?? (resumeFromSnapshot ? snapshot.state : undefined) const restoreTrusted = resumeFromSnapshot || Boolean(runFromBlock?.sourceExecutionId) @@ -945,6 +977,9 @@ async function executeWorkflowCoreImpl( workflowId, ...(executionId ? { executionId } : {}), principal: metadata.principal, + currentWorkflow: deploymentVersionId + ? { workflowId, mode: 'deployment', deploymentVersionId } + : { workflowId, mode: 'draft' }, }, isDeployedContext: metadata.useDraftState !== true, enforceCredentialAccess: metadata.enforceCredentialAccess ?? false, diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.test.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.test.ts index 149c4efced5..664249646a8 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.test.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.test.ts @@ -34,6 +34,7 @@ import { createResumeAttemptTimeoutController, extractResumeBillingAttributionFromSnapshot, PauseResumeManager, + requireResumeDeploymentVersion, updateResumeOutputInAggregationBuffers, } from '@/lib/workflows/executor/human-in-the-loop-manager' import { getAutomaticResumeWaitingMetadata } from '@/lib/workflows/executor/paused-execution-metadata' @@ -1781,17 +1782,21 @@ describe('PauseResumeManager resume log claims', () => { parentExecutionId: string workflowId: string executionDeadlineAt?: Date - }) => Promise + }) => Promise<{ deploymentVersionId: string | null }> it('atomically stamps the active attempt deadline while claiming a paused log', async () => { const executionDeadlineAt = new Date('2026-08-04T12:00:00.000Z') - dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'log-1' }]) + dbChainMockFns.returning.mockResolvedValueOnce([ + { id: 'log-1', deploymentVersionId: 'deployment-version-old' }, + ]) - await claimResumeExecutionLog({ - parentExecutionId: 'execution-1', - workflowId: 'workflow-1', - executionDeadlineAt, - }) + await expect( + claimResumeExecutionLog({ + parentExecutionId: 'execution-1', + workflowId: 'workflow-1', + executionDeadlineAt, + }) + ).resolves.toEqual({ deploymentVersionId: 'deployment-version-old' }) expect(dbChainMockFns.set).toHaveBeenCalledWith({ status: 'running', @@ -1824,6 +1829,29 @@ describe('PauseResumeManager resume log claims', () => { retryable: false, }) }) + + it('reuses the exact historical version claimed from a deployed run log', () => { + expect(requireResumeDeploymentVersion(false, 'deployment-version-old')).toBe( + 'deployment-version-old' + ) + }) + + it('keeps draft resumes version-free', () => { + expect(requireResumeDeploymentVersion(true, null)).toBeUndefined() + }) + + it.each([ + { useDraftState: true, deploymentVersionId: 'deployment-version-1' }, + { useDraftState: false, deploymentVersionId: null }, + { useDraftState: undefined, deploymentVersionId: null }, + ])( + 'rejects an inconsistent paused mode/version binding', + ({ useDraftState, deploymentVersionId }) => { + expect(() => requireResumeDeploymentVersion(useDraftState, deploymentVersionId)).toThrowError( + expect.objectContaining({ name: 'ResumeAdmissionError', statusCode: 409, retryable: false }) + ) + } + ) }) /** diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts index 2dfef26f5d4..f7b5e481a97 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts @@ -134,6 +134,34 @@ class ResumeAdmissionError extends Error { } } +/** Matches the paused execution mode to the deployment recorded on its durable root log. */ +export function requireResumeDeploymentVersion( + useDraftState: unknown, + deploymentVersionId: string | null +): string | undefined { + if (typeof useDraftState !== 'boolean') { + throw new ResumeAdmissionError('Execution mode is missing from the paused run', 409, false) + } + if (useDraftState) { + if (deploymentVersionId !== null) { + throw new ResumeAdmissionError( + 'Paused draft execution cannot resume from a deployment version', + 409, + false + ) + } + return undefined + } + if (!deploymentVersionId) { + throw new ResumeAdmissionError( + 'Paused deployed execution is missing its deployment version', + 409, + false + ) + } + return deploymentVersionId +} + function isPausedOutputForContext(output: unknown, contextId: string): boolean { if (!isRecordLike(output)) return false const metadata = output._pauseMetadata @@ -1011,7 +1039,7 @@ export class PauseResumeManager { parentExecutionId: string workflowId: string executionDeadlineAt?: Date - }): Promise { + }): Promise<{ deploymentVersionId: string | null }> { const { parentExecutionId, workflowId, executionDeadlineAt } = args const [claimedExecution] = await execDb .update(workflowExecutionLogs) @@ -1023,11 +1051,15 @@ export class PauseResumeManager { inArray(workflowExecutionLogs.status, ['pending', 'paused']) ) ) - .returning({ id: workflowExecutionLogs.id }) + .returning({ + id: workflowExecutionLogs.id, + deploymentVersionId: workflowExecutionLogs.deploymentVersionId, + }) if (!claimedExecution) { throw new ResumeAdmissionError('Execution can no longer be resumed', 409, false) } + return { deploymentVersionId: claimedExecution.deploymentVersionId } } private static async runResumeExecution(args: { @@ -1057,7 +1089,7 @@ export class PauseResumeManager { const parentExecutionId = pausedExecution.executionId const executionDeadlineAt = getExecutionDeadlineAt(externalAbortSignal) - await PauseResumeManager.claimResumeExecutionLog({ + const claimedExecution = await PauseResumeManager.claimResumeExecutionLog({ parentExecutionId, workflowId: pausedExecution.workflowId, executionDeadlineAt, @@ -1072,6 +1104,10 @@ export class PauseResumeManager { const serializedSnapshot = pausedExecution.executionSnapshot as SerializedSnapshot const baseSnapshot = ExecutionSnapshot.fromJSON(serializedSnapshot.snapshot) + const resumeDeploymentVersionId = requireResumeDeploymentVersion( + baseSnapshot.metadata.useDraftState, + claimedExecution.deploymentVersionId + ) const billingAttribution = assertBillingAttributionSnapshot( baseSnapshot.metadata.billingAttribution ) @@ -1804,6 +1840,7 @@ export class PauseResumeManager { includeFileBase64: true, base64MaxBytes: undefined, abortSignal: timeoutController.signal, + ...(resumeDeploymentVersionId ? { resumeDeploymentVersionId } : {}), }) if (resumeSnapshot.metadata.resumeTerminalNoop === true && result.status !== 'cancelled') { diff --git a/apps/sim/stores/settings/dirty/store.test.ts b/apps/sim/stores/settings/dirty/store.test.ts new file mode 100644 index 00000000000..1494e1274b0 --- /dev/null +++ b/apps/sim/stores/settings/dirty/store.test.ts @@ -0,0 +1,35 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' + +describe('settings dirty store', () => { + beforeEach(() => { + useSettingsDirtyStore.getState().reset() + }) + + it('blocks navigation without creating a discard action while a save is in flight', () => { + const leave = vi.fn() + useSettingsDirtyStore.getState().setDirty(true) + useSettingsDirtyStore.getState().setNavigationBlocked(true) + + expect(useSettingsDirtyStore.getState().requestLeave(leave)).toBe(false) + expect(useSettingsDirtyStore.getState().pendingLeave).toBeNull() + useSettingsDirtyStore.getState().confirmLeave() + expect(leave).not.toHaveBeenCalled() + }) + + it('allows the normal discard flow after navigation is unblocked', () => { + const leave = vi.fn() + useSettingsDirtyStore.getState().setDirty(true) + useSettingsDirtyStore.getState().setNavigationBlocked(true) + useSettingsDirtyStore.getState().setNavigationBlocked(false) + + expect(useSettingsDirtyStore.getState().requestLeave(leave)).toBe(false) + expect(useSettingsDirtyStore.getState().pendingLeave).toBe(leave) + useSettingsDirtyStore.getState().confirmLeave() + expect(leave).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/sim/stores/settings/dirty/store.ts b/apps/sim/stores/settings/dirty/store.ts index 49a702d2568..a2422075c17 100644 --- a/apps/sim/stores/settings/dirty/store.ts +++ b/apps/sim/stores/settings/dirty/store.ts @@ -3,9 +3,11 @@ import { devtools } from 'zustand/middleware' interface SettingsDirtyStore { isDirty: boolean + navigationBlocked: boolean /** Leave action deferred until the user confirms discard. */ pendingLeave: (() => void) | null setDirty: (dirty: boolean) => void + setNavigationBlocked: (blocked: boolean) => void /** * Call before leaving the current settings surface. If clean, runs `leave` immediately * and returns `true`. If dirty, stashes `leave` and returns `false` so the shared @@ -22,6 +24,7 @@ interface SettingsDirtyStore { const initialState = { isDirty: false, + navigationBlocked: false, pendingLeave: null as (() => void) | null, } @@ -32,7 +35,11 @@ export const useSettingsDirtyStore = create()( setDirty: (dirty) => set({ isDirty: dirty }), + setNavigationBlocked: (blocked) => + set({ navigationBlocked: blocked, ...(blocked ? { pendingLeave: null } : {}) }), + requestLeave: (leave) => { + if (get().navigationBlocked) return false if (!get().isDirty) { leave() return true @@ -42,7 +49,8 @@ export const useSettingsDirtyStore = create()( }, confirmLeave: () => { - const { pendingLeave } = get() + const { navigationBlocked, pendingLeave } = get() + if (navigationBlocked) return set({ ...initialState }) pendingLeave?.() }, diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index f4ec978be6d..a36ab1a516c 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -3929,6 +3929,11 @@ describe('Managed OAuth Credential Delegation', () => { subjectUserId: 'origin-user', workflowId: 'origin-workflow', executionId: 'origin-execution', + currentWorkflow: { + workflowId: 'current-workflow', + mode: 'deployment' as const, + deploymentVersionId: 'deployment-version-1', + }, } const context = createToolExecutionContext({ userId: 'current-user', @@ -3956,6 +3961,47 @@ describe('Managed OAuth Credential Delegation', () => { scopes: ['https://www.googleapis.com/auth/gmail.readonly'], }) }) + + it('fails before transport when managed credential delegation lacks current workflow authority', async () => { + mockGenerateInternalDelegationToken.mockClear() + mockGenerateInternalToken.mockResolvedValueOnce('legacy-token') + const fetchMock = vi.fn() + global.fetch = Object.assign(fetchMock, { preconnect: vi.fn() }) as typeof fetch + + const context = createToolExecutionContext({ + userId: 'current-user', + workflowId: 'current-workflow', + executionId: 'current-execution', + principal: { + kind: 'session', + userId: 'current-user', + sessionId: 'session-1', + }, + executorDelegationOrigin: { + subjectUserId: 'current-user', + workflowId: 'current-workflow', + executionId: 'current-execution', + principal: { + kind: 'session', + userId: 'current-user', + sessionId: 'session-1', + }, + }, + }) + + const result = await executeTool( + 'gmail_read', + { oauthCredential: 'managed-credential-id' }, + { executionContext: context } + ) + + expect(result).toMatchObject({ + success: false, + error: 'Managed credential delegation is missing current workflow authority', + }) + expect(mockGenerateInternalDelegationToken).not.toHaveBeenCalled() + expect(fetchMock).not.toHaveBeenCalled() + }) }) describe('Copilot Env Variable Reference Resolution', () => { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index b6663ad4296..264239ef154 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -1798,21 +1798,16 @@ async function executeToolImplementation( */ const tokenHeaders: Record = { 'Content-Type': 'application/json' } if (typeof window === 'undefined') { + const managedCredentialDelegation = executionContext?.executorDelegationOrigin + if (managedCredentialDelegation && !managedCredentialDelegation.currentWorkflow) { + throw new Error('Managed credential delegation is missing current workflow authority') + } try { const internalToken = await generateInternalToken(userId) tokenHeaders.Authorization = `Bearer ${internalToken}` } catch (_e) { // Swallow token generation errors; the request will fail and be reported upstream } - const managedCredentialDelegation = - executionContext?.executorDelegationOrigin ?? - (workflowId && executionContext?.principal - ? { - workflowId, - ...(scope.executionId ? { executionId: scope.executionId } : {}), - principal: executionContext.principal, - } - : undefined) if (managedCredentialDelegation) { const delegationHeaders = await buildExecutorDelegationHeaders( managedCredentialDelegation diff --git a/packages/auth/src/principal.ts b/packages/auth/src/principal.ts index ddd5a6dc45f..40cfe21adec 100644 --- a/packages/auth/src/principal.ts +++ b/packages/auth/src/principal.ts @@ -77,8 +77,13 @@ export interface WorkflowExecutionDelegationContext { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } +export type WorkflowExecutionAuthority = + | { workflowId: string; mode: 'draft' } + | { workflowId: string; mode: 'deployment'; deploymentVersionId: string } + export interface WorkflowExecutionDelegatedPrincipal extends DelegatedPrincipalBase { serviceId: 'executor' subjectUserId?: string diff --git a/packages/db/credential-group-resource-policies.ts b/packages/db/credential-group-resource-policies.ts new file mode 100644 index 00000000000..a3c9a67d0b7 --- /dev/null +++ b/packages/db/credential-group-resource-policies.ts @@ -0,0 +1,596 @@ +import type { Sql } from 'postgres' + +export const CREDENTIAL_GROUP_POLICY_BATCH_SIZE = 500 +export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT = 50 +export const CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES = 32 * 1024 + +const ACTOR_ACCESS_SID = 'CredentialGroupActorCredentialAccess' +const WORKFLOW_ACCESS_SID = 'WorkflowCredentialAccess' +const CREDENTIAL_USE_ACTION = 'credential_groups.credentials.use' +const ACTOR_OWNS_CREDENTIAL_CONDITION_KEY = 'credential_group:ActorOwnsCredential' +const DEPLOYMENT_MODE_CONDITION_KEY = 'execution:WorkflowMode' + +interface CredentialGroupActorAccessStatement { + sid: typeof ACTOR_ACCESS_SID + effect: 'allow' + actions: [typeof CREDENTIAL_USE_ACTION] + principals: [{ type: 'credential_group_actor' }] + condition: { + Bool: { + [ACTOR_OWNS_CREDENTIAL_CONDITION_KEY]: true + } + } +} + +interface CredentialGroupWorkflowAccessStatement { + sid: typeof WORKFLOW_ACCESS_SID + effect: 'allow' + actions: [typeof CREDENTIAL_USE_ACTION] + principals: Array<{ type: 'workflow'; workflowId: string }> + condition: { + StringEquals: { + [DEPLOYMENT_MODE_CONDITION_KEY]: 'deployment' + } + } +} + +export interface CredentialGroupWorkflowAccessPolicyDocument { + version: 1 + resource: { + type: 'credential_group' + id: string + } + statements: + | [CredentialGroupActorAccessStatement] + | [CredentialGroupActorAccessStatement, CredentialGroupWorkflowAccessStatement] +} + +export interface MissingCredentialGroupPolicyRow { + id: string + workspaceId: string + createdBy: string | null +} + +export interface StoredCredentialGroupPolicyRow { + id: string + workspaceId: string + resourceId: string + revision: number + documentBytes: number + document: unknown +} + +export interface CredentialGroupPolicyInvariantViolation { + kind: 'missing' | 'workspace_mismatch' | 'orphan' + resourceId: string +} + +export interface CredentialGroupPolicyLifecycleStore { + installLifecycleTrigger(): Promise + listMissingPolicies(afterId: string, limit: number): Promise + insertDefaultPolicies(rows: MissingCredentialGroupPolicyRow[]): Promise + findRelationalInvariantViolation(): Promise + listPolicies(afterId: string, limit: number): Promise +} + +interface ReconcileCredentialGroupPoliciesOptions { + batchSize?: number +} + +export interface CredentialGroupPolicyReconciliationResult { + scannedMissing: number + inserted: number + validated: number +} + +function requireRecord(value: unknown, label: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`${label} must be an object`) + } + return value as Record +} + +function requireExactKeys( + value: Record, + expected: readonly string[], + label: string +): void { + const actual = Object.keys(value).sort() + const canonicalExpected = [...expected].sort() + if ( + actual.length !== canonicalExpected.length || + actual.some((key, index) => key !== canonicalExpected[index]) + ) { + throw new Error(`${label} has an invalid shape`) + } +} + +function requireCanonicalId(value: unknown, label: string): string { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > 128 || + value.trim() !== value + ) { + throw new Error(`${label} must be a canonical identifier`) + } + return value +} + +function createCredentialGroupActorAccessStatement(): CredentialGroupActorAccessStatement { + return { + sid: ACTOR_ACCESS_SID, + effect: 'allow', + actions: [CREDENTIAL_USE_ACTION], + principals: [{ type: 'credential_group_actor' }], + condition: { + Bool: { + [ACTOR_OWNS_CREDENTIAL_CONDITION_KEY]: true, + }, + }, + } +} + +export function createDefaultCredentialGroupPolicyDocument( + credentialGroupId: string +): CredentialGroupWorkflowAccessPolicyDocument { + return { + version: 1, + resource: { + type: 'credential_group', + id: requireCanonicalId(credentialGroupId, 'Credential Group ID'), + }, + statements: [createCredentialGroupActorAccessStatement()], + } +} + +export function parseCredentialGroupPolicyDocument( + value: unknown, + expectedResourceId: string +): CredentialGroupWorkflowAccessPolicyDocument { + const canonicalResourceId = requireCanonicalId(expectedResourceId, 'Expected Credential Group ID') + const document = requireRecord(value, 'Credential Group policy document') + requireExactKeys( + document, + ['version', 'resource', 'statements'], + 'Credential Group policy document' + ) + if (document.version !== 1) throw new Error('Credential Group policy version must be 1') + + const resource = requireRecord(document.resource, 'Credential Group policy resource') + requireExactKeys(resource, ['type', 'id'], 'Credential Group policy resource') + if ( + resource.type !== 'credential_group' || + requireCanonicalId(resource.id, 'Credential Group policy resource ID') !== canonicalResourceId + ) { + throw new Error('Credential Group policy resource does not match its canonical resource') + } + + if ( + !Array.isArray(document.statements) || + document.statements.length < 1 || + document.statements.length > 2 + ) { + throw new Error( + 'Credential Group policy must contain its actor statement and optional workflow statement' + ) + } + + const actorStatement = requireRecord(document.statements[0], 'Credential Group actor statement') + requireExactKeys( + actorStatement, + ['sid', 'effect', 'actions', 'principals', 'condition'], + 'Credential Group actor statement' + ) + if (actorStatement.sid !== ACTOR_ACCESS_SID) { + throw new Error(`Credential Group actor statement SID must be ${ACTOR_ACCESS_SID}`) + } + if (actorStatement.effect !== 'allow') { + throw new Error('Credential Group actor statement effect must be allow') + } + if ( + !Array.isArray(actorStatement.actions) || + actorStatement.actions.length !== 1 || + actorStatement.actions[0] !== CREDENTIAL_USE_ACTION + ) { + throw new Error(`Credential Group actor statement action must be ${CREDENTIAL_USE_ACTION}`) + } + if (!Array.isArray(actorStatement.principals) || actorStatement.principals.length !== 1) { + throw new Error('Credential Group actor statement must contain its actor principal') + } + const actorPrincipal = requireRecord( + actorStatement.principals[0], + 'Credential Group actor principal' + ) + requireExactKeys(actorPrincipal, ['type'], 'Credential Group actor principal') + if (actorPrincipal.type !== 'credential_group_actor') { + throw new Error('Credential Group actor statement must target the Credential Group actor') + } + const actorCondition = requireRecord(actorStatement.condition, 'Credential Group actor condition') + requireExactKeys(actorCondition, ['Bool'], 'Credential Group actor condition') + const actorBool = requireRecord(actorCondition.Bool, 'Credential Group actor Bool condition') + requireExactKeys( + actorBool, + [ACTOR_OWNS_CREDENTIAL_CONDITION_KEY], + 'Credential Group actor Bool condition' + ) + if (actorBool[ACTOR_OWNS_CREDENTIAL_CONDITION_KEY] !== true) { + throw new Error('Credential Group actor statement must require actor credential ownership') + } + + const canonicalActorStatement = createCredentialGroupActorAccessStatement() + if (document.statements.length === 1) { + return { + version: 1, + resource: { type: 'credential_group', id: canonicalResourceId }, + statements: [canonicalActorStatement], + } + } + + const statement = requireRecord(document.statements[1], 'Credential Group workflow statement') + requireExactKeys( + statement, + ['sid', 'effect', 'actions', 'principals', 'condition'], + 'Credential Group workflow statement' + ) + if (statement.sid !== WORKFLOW_ACCESS_SID) { + throw new Error(`Credential Group workflow statement SID must be ${WORKFLOW_ACCESS_SID}`) + } + if (statement.effect !== 'allow') { + throw new Error('Credential Group workflow statement effect must be allow') + } + if ( + !Array.isArray(statement.actions) || + statement.actions.length !== 1 || + statement.actions[0] !== CREDENTIAL_USE_ACTION + ) { + throw new Error(`Credential Group workflow statement action must be ${CREDENTIAL_USE_ACTION}`) + } + if ( + !Array.isArray(statement.principals) || + statement.principals.length === 0 || + statement.principals.length > CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT + ) { + throw new Error( + `Credential Group workflow statement must contain 1-${CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT} principals` + ) + } + + const principals = statement.principals.map((value, index) => { + const principal = requireRecord(value, `Credential Group workflow principal ${index}`) + requireExactKeys( + principal, + ['type', 'workflowId'], + `Credential Group workflow principal ${index}` + ) + if (principal.type !== 'workflow') { + throw new Error(`Credential Group workflow principal ${index} must target a workflow`) + } + return { + type: 'workflow' as const, + workflowId: requireCanonicalId( + principal.workflowId, + `Credential Group workflow principal ${index} workflow ID` + ), + } + }) + for (let index = 1; index < principals.length; index++) { + if (principals[index - 1].workflowId >= principals[index].workflowId) { + throw new Error('Credential Group workflow principals must be unique and sorted') + } + } + + const condition = requireRecord(statement.condition, 'Credential Group workflow condition') + requireExactKeys(condition, ['StringEquals'], 'Credential Group workflow condition') + const stringEquals = requireRecord( + condition.StringEquals, + 'Credential Group workflow StringEquals condition' + ) + requireExactKeys( + stringEquals, + [DEPLOYMENT_MODE_CONDITION_KEY], + 'Credential Group workflow StringEquals condition' + ) + if (stringEquals[DEPLOYMENT_MODE_CONDITION_KEY] !== 'deployment') { + throw new Error('Credential Group workflow statement must require deployed execution') + } + + return { + version: 1, + resource: { type: 'credential_group', id: canonicalResourceId }, + statements: [ + canonicalActorStatement, + { + sid: WORKFLOW_ACCESS_SID, + effect: 'allow', + actions: [CREDENTIAL_USE_ACTION], + principals, + condition: { + StringEquals: { + [DEPLOYMENT_MODE_CONDITION_KEY]: 'deployment', + }, + }, + }, + ], + } +} + +function assertPage( + rows: T[], + afterId: string, + batchSize: number, + label: string +): string | null { + if (rows.length === 0) return null + if (rows.length > batchSize) throw new Error(`${label} returned an oversized page`) + const lastId = rows.at(-1)?.id + if (!lastId || lastId <= afterId) throw new Error(`${label} returned a non-advancing page`) + return lastId +} + +export async function reconcileCredentialGroupResourcePolicies( + store: CredentialGroupPolicyLifecycleStore, + options: ReconcileCredentialGroupPoliciesOptions = {} +): Promise { + const batchSize = options.batchSize ?? CREDENTIAL_GROUP_POLICY_BATCH_SIZE + if ( + !Number.isInteger(batchSize) || + batchSize <= 0 || + batchSize > CREDENTIAL_GROUP_POLICY_BATCH_SIZE + ) { + throw new Error( + `Credential Group policy batch size must be between 1 and ${CREDENTIAL_GROUP_POLICY_BATCH_SIZE}` + ) + } + + await store.installLifecycleTrigger() + const result: CredentialGroupPolicyReconciliationResult = { + scannedMissing: 0, + inserted: 0, + validated: 0, + } + + let afterId = '' + for (;;) { + const rows = await store.listMissingPolicies(afterId, batchSize) + const lastId = assertPage(rows, afterId, batchSize, 'Missing Credential Group policy store') + if (!lastId) break + result.scannedMissing += rows.length + result.inserted += await store.insertDefaultPolicies(rows) + afterId = lastId + } + + const violation = await store.findRelationalInvariantViolation() + if (violation) { + throw new Error( + `Credential Group policy invariant failed: ${violation.kind} policy for ${violation.resourceId}` + ) + } + + afterId = '' + for (;;) { + const rows = await store.listPolicies(afterId, batchSize) + const lastId = assertPage(rows, afterId, batchSize, 'Credential Group policy validation store') + if (!lastId) break + for (const row of rows) { + if (!Number.isInteger(row.revision) || row.revision < 1) { + throw new Error(`Credential Group policy ${row.id} has an invalid revision`) + } + if ( + !Number.isInteger(row.documentBytes) || + row.documentBytes < 0 || + row.documentBytes > CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES + ) { + throw new Error( + `Credential Group policy ${row.id} exceeds the ${CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES}-byte limit` + ) + } + parseCredentialGroupPolicyDocument(row.document, row.resourceId) + } + result.validated += rows.length + afterId = lastId + } + return result +} + +export function createPostgresCredentialGroupPolicyLifecycleStore( + sql: Sql +): CredentialGroupPolicyLifecycleStore { + return { + async installLifecycleTrigger() { + await sql.begin(async (tx) => { + await tx`SET LOCAL lock_timeout = '5s'` + await tx` + CREATE OR REPLACE FUNCTION "public"."sync_credential_group_resource_policy"() + RETURNS trigger + LANGUAGE plpgsql + SET search_path = pg_catalog, public + AS $$ + BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO "public"."resource_policy" ( + "id", + "workspace_id", + "resource_type", + "resource_id", + "revision", + "document", + "created_by", + "updated_by" + ) + VALUES ( + gen_random_uuid()::text, + NEW."workspace_id", + 'credential_group', + NEW."id", + 1, + jsonb_build_object( + 'version', 1, + 'resource', jsonb_build_object('type', 'credential_group', 'id', NEW."id"), + 'statements', jsonb_build_array( + jsonb_build_object( + 'sid', 'CredentialGroupActorCredentialAccess', + 'effect', 'allow', + 'actions', jsonb_build_array('credential_groups.credentials.use'), + 'principals', jsonb_build_array( + jsonb_build_object('type', 'credential_group_actor') + ), + 'condition', jsonb_build_object( + 'Bool', jsonb_build_object( + 'credential_group:ActorOwnsCredential', true + ) + ) + ) + ) + ), + NEW."created_by", + NEW."created_by" + ); + RETURN NEW; + END IF; + + DELETE FROM "public"."resource_policy" + WHERE "workspace_id" = OLD."workspace_id" + AND "resource_type" = 'credential_group' + AND "resource_id" = OLD."id"; + RETURN OLD; + END; + $$ + ` + await tx` + DROP TRIGGER IF EXISTS "credential_group_resource_policy_lifecycle" + ON "public"."credential_group" + ` + await tx` + CREATE TRIGGER "credential_group_resource_policy_lifecycle" + AFTER INSERT OR DELETE ON "public"."credential_group" + FOR EACH ROW + EXECUTE FUNCTION "public"."sync_credential_group_resource_policy"() + ` + }) + }, + + async listMissingPolicies(afterId, limit) { + return sql` + SELECT + cg.id, + cg.workspace_id AS "workspaceId", + cg.created_by AS "createdBy" + FROM credential_group cg + WHERE cg.id > ${afterId} + AND NOT EXISTS ( + SELECT 1 + FROM resource_policy rp + WHERE rp.resource_type = 'credential_group' + AND rp.resource_id = cg.id + ) + ORDER BY cg.id + LIMIT ${limit} + ` + }, + + async insertDefaultPolicies(rows) { + if (rows.length === 0) return 0 + if (rows.length > CREDENTIAL_GROUP_POLICY_BATCH_SIZE) { + throw new Error('Credential Group policy insert exceeded the bounded batch size') + } + const ids = rows.map((row) => row.id) + const inserted = await sql>` + INSERT INTO resource_policy ( + id, + workspace_id, + resource_type, + resource_id, + revision, + document, + created_by, + updated_by + ) + SELECT + gen_random_uuid()::text, + cg.workspace_id, + 'credential_group', + cg.id, + 1, + jsonb_build_object( + 'version', 1, + 'resource', jsonb_build_object('type', 'credential_group', 'id', cg.id), + 'statements', jsonb_build_array( + jsonb_build_object( + 'sid', 'CredentialGroupActorCredentialAccess', + 'effect', 'allow', + 'actions', jsonb_build_array('credential_groups.credentials.use'), + 'principals', jsonb_build_array( + jsonb_build_object('type', 'credential_group_actor') + ), + 'condition', jsonb_build_object( + 'Bool', jsonb_build_object( + 'credential_group:ActorOwnsCredential', true + ) + ) + ) + ) + ), + cg.created_by, + cg.created_by + FROM credential_group cg + WHERE cg.id = ANY(${ids}::text[]) + ON CONFLICT (resource_type, resource_id) DO NOTHING + RETURNING resource_id AS "resourceId" + ` + return inserted.length + }, + + async findRelationalInvariantViolation() { + const [violation] = await sql` + SELECT kind, resource_id AS "resourceId" + FROM ( + SELECT + CASE + WHEN rp.resource_id IS NULL THEN 'missing' + ELSE 'workspace_mismatch' + END AS kind, + cg.id AS resource_id + FROM credential_group cg + LEFT JOIN resource_policy rp + ON rp.resource_type = 'credential_group' + AND rp.resource_id = cg.id + WHERE rp.resource_id IS NULL + OR rp.workspace_id IS DISTINCT FROM cg.workspace_id + + UNION ALL + + SELECT 'orphan' AS kind, rp.resource_id + FROM resource_policy rp + LEFT JOIN credential_group cg ON cg.id = rp.resource_id + WHERE rp.resource_type = 'credential_group' + AND cg.id IS NULL + ) violations + ORDER BY resource_id + LIMIT 1 + ` + return violation ?? null + }, + + async listPolicies(afterId, limit) { + return sql` + SELECT + id, + workspace_id AS "workspaceId", + resource_id AS "resourceId", + revision, + octet_length(document::text)::integer AS "documentBytes", + CASE + WHEN octet_length(document::text) <= ${CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES} + THEN document + ELSE NULL + END AS document + FROM resource_policy + WHERE resource_type = 'credential_group' + AND id > ${afterId} + ORDER BY id + LIMIT ${limit} + ` + }, + } +} diff --git a/packages/db/migrations/0308_glorious_hellion.sql b/packages/db/migrations/0308_glorious_hellion.sql new file mode 100644 index 00000000000..6502a9b9991 --- /dev/null +++ b/packages/db/migrations/0308_glorious_hellion.sql @@ -0,0 +1,18 @@ +CREATE TABLE "resource_policy" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text NOT NULL, + "revision" integer DEFAULT 1 NOT NULL, + "document" jsonb NOT NULL, + "created_by" text, + "updated_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_updated_by_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "resource_policy_resource_unique" ON "resource_policy" USING btree ("resource_type","resource_id");--> statement-breakpoint +CREATE INDEX "resource_policy_workspace_id_idx" ON "resource_policy" USING btree ("workspace_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0308_snapshot.json b/packages/db/migrations/meta/0308_snapshot.json new file mode 100644 index 00000000000..89ec2c73553 --- /dev/null +++ b/packages/db/migrations/meta/0308_snapshot.json @@ -0,0 +1,20296 @@ +{ + "id": "3fec7c35-feae-4dd1-9486-66be9127ab73", + "prevId": "10f18d22-c5c5-427c-931c-7c0245d3227f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": 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 + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": 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_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_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_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_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_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_status_idx": { + "name": "credential_group_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_name_unique": { + "name": "credential_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_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.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_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.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": 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 + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_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_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "managed_oauth", "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.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill" + ] + }, + "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 ed14c4f1c9a..672c6826e23 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2150,6 +2150,13 @@ "when": 1787722270619, "tag": "0307_add_subscription_cycle_close_lagging_idx", "breakpoints": true + }, + { + "idx": 308, + "version": "7", + "when": 1787766070644, + "tag": "0308_glorious_hellion", + "breakpoints": true } ] } diff --git a/packages/db/package.json b/packages/db/package.json index f3ddd8c69a0..8f53871aafe 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ } }, "scripts": { - "db:push": "bunx drizzle-kit push --config=./drizzle.config.ts", + "db:push": "bunx drizzle-kit push --config=./drizzle.config.ts && bun --env-file=.env run ./scripts/reconcile-credential-group-resource-policies.ts", "db:migrate": "bun --env-file=.env run ./scripts/migrate.ts", "db:reconcile-fork-kb-file-ownership": "bun --env-file=.env run ./scripts/reconcile-fork-kb-file-ownership.ts", "db:reconcile-workspace-storage": "bun --env-file=.env run ./scripts/reconcile-workspace-storage.ts", diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 770f32167de..0309ae62beb 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4314,6 +4314,32 @@ export const permissionGroupMember = pgTable( }) ) +/** Versioned statement policy attached to one canonical workspace resource. */ +export const resourcePolicy = pgTable( + 'resource_policy', + { + id: text('id').primaryKey(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + resourceType: text('resource_type').notNull(), + resourceId: text('resource_id').notNull(), + revision: integer('revision').notNull().default(1), + document: jsonb('document').$type().notNull(), + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + updatedBy: text('updated_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + resourceUnique: uniqueIndex('resource_policy_resource_unique').on( + table.resourceType, + table.resourceId + ), + workspaceIdx: index('resource_policy_workspace_id_idx').on(table.workspaceId), + }) +) + /** * Async Jobs - Queue for background job processing (Redis/DB backends) * Used when trigger.dev is not available for async workflow executions diff --git a/packages/db/script-migrations-paused-billing-attribution.test.ts b/packages/db/script-migrations-paused-billing-attribution.test.ts index 6d53d3146e1..1eb6bb4ada7 100644 --- a/packages/db/script-migrations-paused-billing-attribution.test.ts +++ b/packages/db/script-migrations-paused-billing-attribution.test.ts @@ -444,6 +444,7 @@ describe('script migration registry', () => { '0005_repair_unknown_table_row_provenance', '0006_repair_unknown_table_row_provenance_second_pass', '0007_repair_unknown_workspace_file_provenance', + '0008_backfill_credential_group_resource_policies', ]) }) }) diff --git a/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.test.ts b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.test.ts new file mode 100644 index 00000000000..c27f47c9280 --- /dev/null +++ b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.test.ts @@ -0,0 +1,356 @@ +/** + * @vitest-environment node + */ +import { readFile } from 'node:fs/promises' +import type { Sql } from 'postgres' +import { describe, expect, it, vi } from 'vitest' +import { + CREDENTIAL_GROUP_POLICY_BATCH_SIZE, + CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES, + CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT, + type CredentialGroupPolicyLifecycleStore, + createDefaultCredentialGroupPolicyDocument, + createPostgresCredentialGroupPolicyLifecycleStore, + type MissingCredentialGroupPolicyRow, + parseCredentialGroupPolicyDocument, + reconcileCredentialGroupResourcePolicies, + type StoredCredentialGroupPolicyRow, +} from '../credential-group-resource-policies' + +const WORKFLOW_POLICY = (id: string, workflowIds: string[]) => ({ + version: 1 as const, + resource: { type: 'credential_group' as const, id }, + statements: [ + createDefaultCredentialGroupPolicyDocument(id).statements[0], + { + sid: 'WorkflowCredentialAccess' as const, + effect: 'allow' as const, + actions: ['credential_groups.credentials.use'] as const, + principals: workflowIds.map((workflowId) => ({ type: 'workflow' as const, workflowId })), + condition: { StringEquals: { 'execution:WorkflowMode': 'deployment' as const } }, + }, + ] as const, +}) + +function normalizeSql(value: string): string { + return value.replace(/\s+/g, ' ').trim() +} + +describe('Credential Group resource policy lifecycle', () => { + it('accepts only the canonical actor-only or actor-plus-workflow document', () => { + expect( + parseCredentialGroupPolicyDocument( + createDefaultCredentialGroupPolicyDocument('group-1'), + 'group-1' + ) + ).toEqual(createDefaultCredentialGroupPolicyDocument('group-1')) + expect( + parseCredentialGroupPolicyDocument( + WORKFLOW_POLICY('group-1', ['workflow-1', 'workflow-2']), + 'group-1' + ) + ).toEqual(WORKFLOW_POLICY('group-1', ['workflow-1', 'workflow-2'])) + }) + + it.each([ + ['wrong target', WORKFLOW_POLICY('group-2', ['workflow-1'])], + [ + 'multiple statements', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], + WORKFLOW_POLICY('group-1', ['workflow-2']).statements[1], + ], + }, + ], + [ + 'noncanonical SID', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], sid: 'AnotherRule' }, + ], + }, + ], + [ + 'deny effect', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], effect: 'deny' }, + ], + }, + ], + [ + 'extra action', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], + actions: ['credential_groups.credentials.use', 'credential_groups.read'], + }, + ], + }, + ], + [ + 'non-workflow principal', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], + principals: [{ type: 'user', userId: 'user-1' }], + }, + ], + }, + ], + ['unsorted principals', WORKFLOW_POLICY('group-1', ['workflow-2', 'workflow-1'])], + ['duplicate principals', WORKFLOW_POLICY('group-1', ['workflow-1', 'workflow-1'])], + [ + 'draft condition', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], + condition: { StringEquals: { 'execution:WorkflowMode': 'draft' } }, + }, + ], + }, + ], + [ + 'array condition', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], + condition: { StringEquals: { 'execution:WorkflowMode': ['deployment'] } }, + }, + ], + }, + ], + [ + 'extra field', + { + ...WORKFLOW_POLICY('group-1', ['workflow-1']), + statements: [ + createDefaultCredentialGroupPolicyDocument('group-1').statements[0], + { ...WORKFLOW_POLICY('group-1', ['workflow-1']).statements[1], note: 'unsupported' }, + ], + }, + ], + [ + 'too many principals', + WORKFLOW_POLICY( + 'group-1', + Array.from( + { length: CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT + 1 }, + (_, index) => `workflow-${String(index).padStart(3, '0')}` + ) + ), + ], + ])('rejects %s', (_label, document) => { + expect(() => parseCredentialGroupPolicyDocument(document, 'group-1')).toThrow() + }) + + it('installs lifecycle first, backfills bounded pages, and preserves valid policies on rerun', async () => { + const missingRows: MissingCredentialGroupPolicyRow[] = [ + { id: 'group-2', workspaceId: 'workspace-1', createdBy: 'user-1' }, + ] + const policies: StoredCredentialGroupPolicyRow[] = [ + { + id: 'policy-1', + workspaceId: 'workspace-1', + resourceId: 'group-1', + revision: 7, + documentBytes: 1024, + document: WORKFLOW_POLICY('group-1', ['workflow-1']), + }, + ] + const calls: string[] = [] + const store: CredentialGroupPolicyLifecycleStore = { + async installLifecycleTrigger() { + calls.push('install') + }, + async listMissingPolicies(afterId, limit) { + calls.push(`missing:${afterId}:${limit}`) + return missingRows.filter((row) => row.id > afterId).slice(0, limit) + }, + async insertDefaultPolicies(rows) { + calls.push(`insert:${rows.length}`) + for (const row of rows) { + missingRows.splice( + missingRows.findIndex((candidate) => candidate.id === row.id), + 1 + ) + policies.push({ + id: `policy-${row.id}`, + workspaceId: row.workspaceId, + resourceId: row.id, + revision: 1, + documentBytes: 128, + document: createDefaultCredentialGroupPolicyDocument(row.id), + }) + } + return rows.length + }, + async findRelationalInvariantViolation() { + calls.push('invariants') + return null + }, + async listPolicies(afterId, limit) { + calls.push(`validate:${afterId}:${limit}`) + return [...policies] + .filter((row) => row.id > afterId) + .sort((left, right) => left.id.localeCompare(right.id)) + .slice(0, limit) + }, + } + + await expect( + reconcileCredentialGroupResourcePolicies(store, { batchSize: 2 }) + ).resolves.toEqual({ scannedMissing: 1, inserted: 1, validated: 2 }) + expect(policies[0]).toMatchObject({ + revision: 7, + document: WORKFLOW_POLICY('group-1', ['workflow-1']), + }) + expect(calls[0]).toBe('install') + + calls.length = 0 + await expect( + reconcileCredentialGroupResourcePolicies(store, { batchSize: 2 }) + ).resolves.toEqual({ scannedMissing: 0, inserted: 0, validated: 2 }) + expect(calls[0]).toBe('install') + }) + + it('fails fast on malformed rows, relational violations, and invalid page bounds', async () => { + const base: CredentialGroupPolicyLifecycleStore = { + installLifecycleTrigger: vi.fn(), + listMissingPolicies: vi.fn().mockResolvedValue([]), + insertDefaultPolicies: vi.fn(), + findRelationalInvariantViolation: vi.fn().mockResolvedValue(null), + listPolicies: vi.fn().mockResolvedValue([]), + } + + await expect( + reconcileCredentialGroupResourcePolicies(base, { + batchSize: CREDENTIAL_GROUP_POLICY_BATCH_SIZE + 1, + }) + ).rejects.toThrow('batch size must be between') + + await expect( + reconcileCredentialGroupResourcePolicies({ + ...base, + findRelationalInvariantViolation: vi + .fn() + .mockResolvedValue({ kind: 'orphan', resourceId: 'group-1' }), + }) + ).rejects.toThrow('orphan policy for group-1') + + await expect( + reconcileCredentialGroupResourcePolicies({ + ...base, + listMissingPolicies: vi + .fn() + .mockResolvedValue([{ id: '', workspaceId: 'workspace-1', createdBy: null }]), + }) + ).rejects.toThrow('non-advancing page') + + await expect( + reconcileCredentialGroupResourcePolicies({ + ...base, + listPolicies: vi.fn().mockResolvedValueOnce([ + { + id: 'policy-1', + workspaceId: 'workspace-1', + resourceId: 'group-1', + revision: 0, + documentBytes: 128, + document: createDefaultCredentialGroupPolicyDocument('group-1'), + }, + ]), + }) + ).rejects.toThrow('invalid revision') + + await expect( + reconcileCredentialGroupResourcePolicies({ + ...base, + listPolicies: vi.fn().mockResolvedValueOnce([ + { + id: 'policy-1', + workspaceId: 'workspace-1', + resourceId: 'group-1', + revision: 1, + documentBytes: CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES + 1, + document: null, + }, + ]), + }) + ).rejects.toThrow('exceeds the 32768-byte limit') + }) + + it('uses idempotent lifecycle DDL and bounded canonical inserts', async () => { + const queries: string[] = [] + const query = vi.fn((strings: TemplateStringsArray) => { + const text = normalizeSql(strings.join('?')) + queries.push(text) + if (text.includes('INSERT INTO resource_policy')) { + return Promise.resolve([{ resourceId: 'group-1' }]) + } + return Promise.resolve([]) + }) + const sql = query as unknown as Sql + sql.begin = vi.fn(async (callback) => callback(sql)) as Sql['begin'] + const store = createPostgresCredentialGroupPolicyLifecycleStore(sql) + + await store.installLifecycleTrigger() + await expect( + store.insertDefaultPolicies([ + { id: 'group-1', workspaceId: 'workspace-1', createdBy: 'user-1' }, + ]) + ).resolves.toBe(1) + await store.listPolicies('', 2) + + expect(queries).toHaveLength(6) + expect(queries[0]).toBe("SET LOCAL lock_timeout = '5s'") + expect(queries[1]).toContain('CREATE OR REPLACE FUNCTION') + expect(queries[1]).toContain("'sid', 'CredentialGroupActorCredentialAccess'") + expect(queries[1]).toContain("'credential_group:ActorOwnsCredential', true") + expect(queries[2]).toContain('DROP TRIGGER IF EXISTS') + expect(queries[3]).toContain('CREATE TRIGGER') + expect(queries[4]).toContain('ON CONFLICT (resource_type, resource_id) DO NOTHING') + expect(queries[5]).toContain('octet_length(document::text)') + expect(queries[5]).toContain('THEN document ELSE NULL') + }) + + it('keeps table creation in 0308 and lifecycle reconciliation in the db:push post-step', async () => { + const migration = normalizeSql( + await readFile(new URL('../migrations/0308_glorious_hellion.sql', import.meta.url), 'utf8') + ) + const packageJson = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8') + ) as { scripts: Record } + const helperSource = await readFile( + new URL('../credential-group-resource-policies.ts', import.meta.url), + 'utf8' + ) + + expect(migration).toContain('CREATE TABLE "resource_policy"') + expect(migration).not.toContain('credential_group_resource_policy_lifecycle') + expect(packageJson.scripts['db:push']).toContain( + 'scripts/reconcile-credential-group-resource-policies.ts' + ) + expect(helperSource).not.toContain('LegacyResourcePolicy') + expect(helperSource).not.toContain("document ? 'grants'") + }) +}) diff --git a/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.ts b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.ts new file mode 100644 index 00000000000..3585b3f87ed --- /dev/null +++ b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.ts @@ -0,0 +1,18 @@ +import { createLogger } from '@sim/logger' +import { + createPostgresCredentialGroupPolicyLifecycleStore, + reconcileCredentialGroupResourcePolicies, +} from '../credential-group-resource-policies' +import type { ScriptMigration } from './types' + +const logger = createLogger('CredentialGroupResourcePolicyMigration') + +export const backfillCredentialGroupResourcePolicies: ScriptMigration = { + name: '0008_backfill_credential_group_resource_policies', + async up(sql) { + const result = await reconcileCredentialGroupResourcePolicies( + createPostgresCredentialGroupPolicyLifecycleStore(sql) + ) + logger.info('Credential Group policy reconciliation completed', result) + }, +} diff --git a/packages/db/script-migrations/index.ts b/packages/db/script-migrations/index.ts index b845d7f4ebc..c70d2b524c1 100644 --- a/packages/db/script-migrations/index.ts +++ b/packages/db/script-migrations/index.ts @@ -6,6 +6,7 @@ import { backfillForkKnowledgeBaseFileOwnership } from './0004_backfill_fork_kb_ import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance' import { repairUnknownTableRowProvenanceSecondPass } from './0006_repair_unknown_table_row_provenance_second_pass' import { repairUnknownWorkspaceFileProvenance } from './0007_repair_unknown_workspace_file_provenance' +import { backfillCredentialGroupResourcePolicies } from './0008_backfill_credential_group_resource_policies' import type { ScriptMigration } from './types' export type { ScriptMigration } from './types' @@ -23,6 +24,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [ repairUnknownTableRowProvenance, repairUnknownTableRowProvenanceSecondPass, repairUnknownWorkspaceFileProvenance, + backfillCredentialGroupResourcePolicies, ] /** diff --git a/packages/db/scripts/reconcile-credential-group-resource-policies.ts b/packages/db/scripts/reconcile-credential-group-resource-policies.ts new file mode 100644 index 00000000000..f7e77894662 --- /dev/null +++ b/packages/db/scripts/reconcile-credential-group-resource-policies.ts @@ -0,0 +1,29 @@ +import { createLogger } from '@sim/logger' +import postgres from 'postgres' +import { + createPostgresCredentialGroupPolicyLifecycleStore, + reconcileCredentialGroupResourcePolicies, +} from '../credential-group-resource-policies' + +const logger = createLogger('CredentialGroupResourcePolicyReconciliation') +const url = process.env.DATABASE_URL + +if (!url) { + throw new Error('Missing DATABASE_URL') +} + +const sql = postgres(url, { + max: 1, + connect_timeout: 10, + max_lifetime: null, + connection: { application_name: 'sim-credential-group-policy-reconcile' }, +}) + +try { + const result = await reconcileCredentialGroupResourcePolicies( + createPostgresCredentialGroupPolicyLifecycleStore(sql) + ) + logger.info('Credential Group policy reconciliation completed', result) +} finally { + await sql.end() +} diff --git a/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx b/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx new file mode 100644 index 00000000000..e6a916f868e --- /dev/null +++ b/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx @@ -0,0 +1,48 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import { ChipDropdown } from './chip-dropdown' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function mount(fullWidth: boolean): HTMLButtonElement { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render( + + ) + ) + + const trigger = container.querySelector('button') + if (!trigger) throw new Error('ChipDropdown did not render a trigger') + return trigger +} + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('ChipDropdown', () => { + it('fills its container when fullWidth is enabled', () => { + expect(mount(true).className).toContain('w-full') + }) + + it('keeps its intrinsic width by default', () => { + expect(mount(false).className).not.toContain('w-full') + }) +}) diff --git a/packages/emcn/src/components/chip-select/chip-select.dom.test.tsx b/packages/emcn/src/components/chip-select/chip-select.dom.test.tsx new file mode 100644 index 00000000000..e55d7033595 --- /dev/null +++ b/packages/emcn/src/components/chip-select/chip-select.dom.test.tsx @@ -0,0 +1,52 @@ +/** + * @vitest-environment jsdom + */ + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import { ChipSelect } from './chip-select' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function mount(): HTMLButtonElement { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render( + + ) + ) + + const trigger = container.querySelector('button') + if (!trigger) throw new Error('ChipSelect did not render a trigger') + return trigger +} + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('ChipSelect', () => { + it('forwards field accessibility attributes to its trigger', () => { + const trigger = mount() + + expect(trigger.getAttribute('aria-label')).toBe('Workflow') + expect(trigger.getAttribute('aria-required')).toBe('true') + expect(trigger.getAttribute('aria-invalid')).toBe('true') + expect(trigger.getAttribute('aria-describedby')).toBe('workflow-error') + }) +}) diff --git a/packages/emcn/src/components/chip-select/chip-select.tsx b/packages/emcn/src/components/chip-select/chip-select.tsx index 94b4da1885e..494813a70d1 100644 --- a/packages/emcn/src/components/chip-select/chip-select.tsx +++ b/packages/emcn/src/components/chip-select/chip-select.tsx @@ -86,6 +86,12 @@ export interface ChipSelectProps { contentClassName?: string /** Accessible label for the trigger. */ 'aria-label'?: string + /** Marks the trigger as required. */ + 'aria-required'?: React.AriaAttributes['aria-required'] + /** Marks the trigger as invalid. */ + 'aria-invalid'?: React.AriaAttributes['aria-invalid'] + /** Id of hint or error content describing the trigger. */ + 'aria-describedby'?: React.AriaAttributes['aria-describedby'] /** * Forwarded to the underlying `DropdownMenu`'s Radix `modal` prop * (default `true`, matching Radix). Set `false` when an `onChange` handler @@ -148,6 +154,9 @@ export function ChipSelect({ className, contentClassName, 'aria-label': ariaLabel, + 'aria-required': ariaRequired, + 'aria-invalid': ariaInvalid, + 'aria-describedby': ariaDescribedBy, modal, }: ChipSelectProps) { const [query, setQuery] = React.useState('') @@ -249,10 +258,13 @@ export function ChipSelect({ type='button' disabled={disabled} aria-label={ariaLabel} + aria-required={ariaRequired} + aria-invalid={ariaInvalid} + aria-describedby={ariaDescribedBy} className={cn( chipVariants({ variant: 'filled', fullWidth }), TRIGGER_BORDER_CLASS, - fullWidth ? 'w-full justify-between' : 'w-fit max-w-[240px]', + fullWidth ? 'justify-between' : 'w-fit max-w-[240px]', className )} > diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index aa80a5d225a..a52b3740e1e 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -68,7 +68,7 @@ const chipVariants = cva( border: `shadow-[0_0_0_1px_rgba(28,40,64,0.08),0_1px_3px_0_rgba(28,40,64,0.1)] ${chipHoverSurfaceClass} dark:shadow-[0_0_0_1px_var(--border-1),0_1px_3px_0_rgba(0,0,0,0.3)]`, }, active: { true: '', false: '' }, - fullWidth: { true: 'flex', false: 'inline-flex' }, + fullWidth: { true: 'flex w-full', false: 'inline-flex' }, }, compoundVariants: [ { variant: ['default', 'filled'], active: false, className: chipHoverSurfaceClass }, diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index d3059f4276d..9fddbb4852d 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1191,6 +1191,18 @@ export const schemaMock = { createdAt: 'credentialGroup.createdAt', updatedAt: 'credentialGroup.updatedAt', }, + resourcePolicy: { + id: 'id', + workspaceId: 'workspaceId', + resourceType: 'resourceType', + resourceId: 'resourceId', + revision: 'revision', + document: 'document', + createdBy: 'createdBy', + updatedBy: 'updatedBy', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, credentialGroupEnrollmentStatusEnum: { enumValues: ['invited', 'delivery_failed', 'in_progress', 'completed', 'revoked'] as const, }, diff --git a/packages/testing/src/mocks/workflows-persistence-utils.mock.ts b/packages/testing/src/mocks/workflows-persistence-utils.mock.ts index 57fb6761db6..66b0d073ea4 100644 --- a/packages/testing/src/mocks/workflows-persistence-utils.mock.ts +++ b/packages/testing/src/mocks/workflows-persistence-utils.mock.ts @@ -20,6 +20,7 @@ import { vi } from 'vitest' export const workflowsPersistenceUtilsMockFns = { mockBlockExistsInDeployment: vi.fn(), mockLoadDeployedWorkflowState: vi.fn(), + mockLoadWorkflowDeploymentVersionState: vi.fn(), mockMigrateAgentBlocksToMessagesFormat: vi.fn(), mockLoadWorkflowFromNormalizedTables: vi.fn(), mockSaveWorkflowToNormalizedTables: vi.fn(), @@ -43,6 +44,8 @@ export const workflowsPersistenceUtilsMockFns = { export const workflowsPersistenceUtilsMock = { blockExistsInDeployment: workflowsPersistenceUtilsMockFns.mockBlockExistsInDeployment, loadDeployedWorkflowState: workflowsPersistenceUtilsMockFns.mockLoadDeployedWorkflowState, + loadWorkflowDeploymentVersionState: + workflowsPersistenceUtilsMockFns.mockLoadWorkflowDeploymentVersionState, migrateAgentBlocksToMessagesFormat: workflowsPersistenceUtilsMockFns.mockMigrateAgentBlocksToMessagesFormat, loadWorkflowFromNormalizedTables: