From 45ab0f2a2032cb64a6ca9c86d8bff1a688404b28 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 11 Sep 2026 15:52:58 -0700 Subject: [PATCH 01/15] fix(knowledge): simplify connector management (#7788) * fix(knowledge): simplify connector management * fix(knowledge): keep history collapse accessible --- apps/docs/content/docs/search/confluence.mdx | 2 +- .../docs/search/connect-your-account.mdx | 2 +- apps/docs/content/docs/search/gitlab.mdx | 2 +- apps/docs/content/docs/search/index.mdx | 2 +- .../connect-oauth-modal.test.tsx | 4 + .../connect-oauth-modal.tsx | 19 +- .../connector-document-status-filter.tsx | 39 ++++ .../connector-documents.tsx | 58 +++--- .../connectors-section/connector-actions.tsx | 121 ++++++++---- .../connectors-section/connector-recovery.tsx | 32 ++-- .../connector-sync-state.ts | 5 - .../connectors-section.test.tsx | 165 ++++++++++------ .../connectors-section/connectors-section.tsx | 180 ++++++++---------- .../use-connector-actions.ts | 38 +--- .../connector-documents-tab.test.tsx | 82 +++++++- .../connector-documents-tab.tsx | 33 +++- .../edit-connector-modal.tsx | 42 ++-- apps/sim/hooks/queries/kb/connectors.ts | 10 +- .../src/components/chip-modal/chip-modal.tsx | 2 +- 19 files changed, 518 insertions(+), 320 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx diff --git a/apps/docs/content/docs/search/confluence.mdx b/apps/docs/content/docs/search/confluence.mdx index 1f7887a2de1..d6f5b3f806b 100644 --- a/apps/docs/content/docs/search/confluence.mdx +++ b/apps/docs/content/docs/search/confluence.mdx @@ -132,7 +132,7 @@ Central sources combine space permissions, page and ancestor restrictions, and g Open **Settings → Sources → Confluence**, then a source's **Documents**, **Settings**, or **Sync history**. Invite teammates through **Settings → Members → Invite** or SSO, then have them connect through **Integrations**. **People → Request connections** only requests a provider connection; it does not invite people to the organization. -Syncing runs automatically. Admins can use **Sync now** for an immediate update, **Pause syncing** to stop scheduled syncs, or **Resume syncing** to restart them. **Full resync**, available for service-account connections, fetches unchanged content again and asks for confirmation. Successful manual syncs have a one-minute cooldown; failed syncs can be retried immediately. +Syncing runs automatically. Admins can use **Sync now** for an immediate update, **Pause syncing** to stop scheduled syncs, or **Resume syncing** to restart them. Successful manual syncs have a one-minute cooldown; failed syncs can be retried immediately. ## Troubleshooting diff --git a/apps/docs/content/docs/search/connect-your-account.mdx b/apps/docs/content/docs/search/connect-your-account.mdx index 353fc520956..b82cdf817a2 100644 --- a/apps/docs/content/docs/search/connect-your-account.mdx +++ b/apps/docs/content/docs/search/connect-your-account.mdx @@ -78,7 +78,7 @@ On the main **Integrations** page, select **Reconnect** beside the integration i Admins manage setup from **Settings → Sources**. Open an integration, then its connection to see **Documents**, **Settings**, and **Sync history**. **People** shows account contributors across integrations and supports filtering by integration. This does not grant the admin access to every document. -Syncing runs automatically. Admins can use **Sync now** when they need an update; another manual run is available 60 seconds after a successful sync finishes. Failed or partial runs can be retried immediately. The connection header also offers **Pause syncing** or **Resume syncing**, and **Remove connection**. Where supported, **Full resync** fetches and reindexes all content and asks for confirmation first. +Syncing runs automatically. Admins can use **Sync now** when they need an update; another manual run is available 60 seconds after a successful sync finishes. Failed or partial runs can be retried immediately. The connection header also offers **Pause syncing** or **Resume syncing**, and **Remove connection**. Removing a Search connection also removes its indexed documents from Sim. The originals remain in the connected app. diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx index 7dbc4f15fa5..55848547f6c 100644 --- a/apps/docs/content/docs/search/gitlab.mdx +++ b/apps/docs/content/docs/search/gitlab.mdx @@ -146,7 +146,7 @@ Open a project to use these administrator actions: | **Settings** | Change the token, project, filters, or CSV permissions. | | **Remove connection** | Confirm removal of the connection and its indexed documents. Documents cannot be retained without the connection that maintains their permissions. | -GitLab does not expose a separate **Full resync** action. Each sync checks the selected content. CSV grants change only when you replace the files. +Each sync checks the selected content. CSV grants change only when you replace the files. ## Troubleshooting diff --git a/apps/docs/content/docs/search/index.mdx b/apps/docs/content/docs/search/index.mdx index 68b53e20f2a..063d27d9243 100644 --- a/apps/docs/content/docs/search/index.mdx +++ b/apps/docs/content/docs/search/index.mdx @@ -104,7 +104,7 @@ Edit **Settings** to change an existing connection's filters. Adding another con **Sync using** shows the method selected when the source was created. Add a new connection to change that method. To replace a supported indexing credential, select its replacement and use **Change service account** or **Change account**, as shown. -The connection header offers **Sync now**, **Pause syncing** or **Resume syncing**, **Remove connection**, and, where supported, **Full resync**. Full resync fetches all content again and requires confirmation. Manual runs have a 60-second cooldown after a successful sync finishes; failed or partial runs can be retried immediately. **Pause syncing** becomes available when the current sync finishes. +The connection header offers **Sync now**, **Pause syncing** or **Resume syncing**, and **Remove connection**. Manual runs have a 60-second cooldown after a successful sync finishes; failed or partial runs can be retried immediately. **Pause syncing** becomes available when the current sync finishes. To deactivate an entire integration, open it from **Settings → Sources**, select **Deactivate**, and confirm. Its content becomes unavailable in Search, Assistant, and MCP; saved connections remain. Select **Activate** on that integration to enable it again. diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.test.tsx index b94535be58c..c6050bf0f43 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.test.tsx @@ -347,6 +347,8 @@ describe('ConnectOAuthModal reauthorization', () => { }, }) + expect(container.querySelector('header')).toHaveTextContent('Reconnect Slack') + expect(container).not.toHaveTextContent('tool requires access') await clickConnect() expect(mocks.writeOAuthReturnContext).toHaveBeenCalledExactlyOnceWith( @@ -424,6 +426,8 @@ describe('ConnectOAuthModal reauthorization', () => { }, }) + expect(container.querySelector('header')).toHaveTextContent('Connect Slack') + expect(container).toHaveTextContent('tool requires access') await clickConnect() expect(mocks.writeOAuthReturnContext).toHaveBeenCalledOnce() diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx index 4ec62347415..e624f434d0e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx @@ -506,7 +506,16 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { ? `An integration named "${existingCredential.displayName}" already exists.` : undefined) - const title = `Connect ${providerName}` + const isConnectorReconnect = !isConnect && props.returnContext?.origin === 'kb-connectors' + const connectLabel = isConnectorReconnect + ? newScopes.length > 0 + ? 'Update access' + : 'Reconnect' + : 'Connect' + const title = + isConnectorReconnect && newScopes.length > 0 + ? `Update ${providerName} access` + : `${connectLabel} ${providerName}` return ( @@ -519,7 +528,11 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { {!isConnect && (

- The "{props.toolName}" tool requires access to your account. + {isConnectorReconnect + ? newScopes.length > 0 + ? 'Approve the requested permissions to continue syncing.' + : `Continue to ${providerName} to restore this connection.` + : `The "${props.toolName}" tool requires access to your account.`}

)} @@ -651,7 +664,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { : undefined } primaryAction={{ - label: isPending ? 'Connecting...' : 'Connect', + label: isPending ? 'Connecting...' : connectLabel, onClick: handleConnect, disabled: isDisabled, }} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx new file mode 100644 index 00000000000..749b08d852c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx @@ -0,0 +1,39 @@ +'use client' + +import { ChipDropdown } from '@sim/emcn' +import type { + ConnectorDocumentFilter, + ConnectorDocumentsData, +} from '@/lib/api/contracts/knowledge/connectors' + +interface ConnectorDocumentStatusFilterProps { + filter: ConnectorDocumentFilter + onFilterChange: (filter: ConnectorDocumentFilter) => void + counts?: ConnectorDocumentsData['counts'] + isLoading: boolean +} + +export function ConnectorDocumentStatusFilter({ + filter, + onFilterChange, + counts = { active: 0, excluded: 0, failed: 0, skipped: 0 }, + isLoading, +}: ConnectorDocumentStatusFilterProps) { + return ( + { + if (value === 'active' || value === 'excluded' || value === 'failed' || value === 'skipped') + onFilterChange(value) + }} + matchTriggerWidth={false} + options={[ + { value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` }, + { value: 'excluded', label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})` }, + { value: 'failed', label: isLoading ? 'Failed' : `Failed (${counts.failed})` }, + { value: 'skipped', label: isLoading ? 'Skipped' : `Skipped (${counts.skipped})` }, + ]} + /> + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents.tsx index a63cf633a05..7323a54c0ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents.tsx @@ -1,10 +1,11 @@ 'use client' -import { Chip, ChipDropdown, ChipInput, ChipLink, Skeleton } from '@sim/emcn' +import { Chip, ChipInput, ChipLink, Skeleton } from '@sim/emcn' import { RefreshCw, Search, SquareArrowUpRight } from '@sim/emcn/icons' import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors' import type { ResourceScope } from '@/lib/core/resource-scope' import { getDocumentIndexingStatus } from '@/lib/knowledge/documents/types' +import { ConnectorDocumentStatusFilter } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter' import { SettingsEmptyState, SettingsQueryErrorState, @@ -25,6 +26,7 @@ interface ConnectorDocumentsProps { connectorId: string search?: string searchControl?: { value: string; onChange: (value: string) => void } + showToolbar?: boolean progressScope?: ResourceScope isSearchIndex?: boolean syncing?: boolean @@ -38,6 +40,7 @@ export function ConnectorDocuments({ filter, search, searchControl, + showToolbar = true, progressScope, isSearchIndex = false, syncing, @@ -79,41 +82,26 @@ export function ConnectorDocuments({ {isSearchIndex && (

Documents you can access

)} -
- {searchControl && ( - searchControl.onChange(event.target.value)} - autoComplete='off' - className='min-w-0 flex-1' + {showToolbar && ( +
+ {searchControl && ( + searchControl.onChange(event.target.value)} + autoComplete='off' + className='min-w-0 flex-1' + /> + )} + - )} - { - if ( - value === 'active' || - value === 'excluded' || - value === 'failed' || - value === 'skipped' - ) - onFilterChange(value) - }} - matchTriggerWidth={false} - options={[ - { value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` }, - { - value: 'excluded', - label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})`, - }, - { value: 'failed', label: isLoading ? 'Failed' : `Failed (${counts.failed})` }, - { value: 'skipped', label: isLoading ? 'Skipped' : `Skipped (${counts.skipped})` }, - ]} - /> -
+
+ )}
{query.isError && !query.isFetchNextPageError ? ( + history?: { + expanded: boolean + contentId: string + onToggle: () => void + } +} + +export function ConnectorActions({ state, triggerRef, history }: ConnectorActionsProps) { + if (!state.canEdit && !history) return null + const actions = orderHeaderActions([ + ...state.actions, + ...(history + ? [ + { + id: 'history', + text: history.expanded ? 'Hide history' : 'Sync history', + onSelect: history.onToggle, + }, + ] + : []), + ]) return ( -
-
- -
- -
+ + + + + + {actions.map(({ action }, index) => ( + + {action.id === 'delete' && index > 0 && } + + + ))} + + + ) +} + +interface ConnectorActionMenuItemProps { + action: SettingsAction + history?: ConnectorActionsProps['history'] +} + +function ConnectorActionMenuItem({ action, history }: ConnectorActionMenuItemProps) { + const item = ( + + {action.text} + + ) + return action.tooltip ? ( + + + {item} + + {action.tooltip} + + ) : ( + item ) } @@ -30,7 +101,7 @@ interface ConnectorActionFeedbackProps { export function ConnectorActionFeedback({ state }: ConnectorActionFeedbackProps) { const deleteDocumentsId = useId() if (!state.canEdit) return null - const { removal, fullResync } = state + const { removal } = state return ( <> {state.error && ( @@ -38,22 +109,6 @@ export function ConnectorActionFeedback({ state }: ConnectorActionFeedbackProps) {state.error.message} )} - - {fullResync.error?.message} - { - if ( - showOAuthModal && - (requiresAccountSettings || - (connector.credentialId && !selectedCredential && !credentialsLoading)) - ) { + if (showOAuthModal && requiresAccountSettings) { setShowOAuthModal(false) } - }, [ - showOAuthModal, - connector.credentialId, - selectedCredential, - credentialsLoading, - requiresAccountSettings, - ]) + }, [showOAuthModal, requiresAccountSettings]) function openReconnect() { if (!canEdit || disabled || requiresAccountSettings) return @@ -102,13 +93,14 @@ export function ConnectorRecovery({ )} {connector.status === 'disabled' ? ( ({ children, disabled, onSelect, - }: { - children?: ReactNode - disabled?: boolean + ...props + }: React.ButtonHTMLAttributes & { onSelect: () => void }) => ( - ), @@ -252,14 +251,29 @@ vi.mock('@/hooks/use-credential-refresh-triggers', () => ({ })) import { - ConnectorActions, + ConnectorActions as ConnectorActionMenu, ConnectorRecovery, ConnectorSyncHistory, ConnectorsSection, SyncHistory, } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section' +import { ConnectorActionFeedback } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions' +import { + type ConnectorActionsOptions, + useConnectorActions, +} from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions' import { type ConnectorData, useConnectorDetail } from '@/hooks/queries/kb/connectors' +function ConnectorActions(props: ConnectorActionsOptions) { + const state = useConnectorActions(props) + return ( + <> + + + + ) +} + let root: Root | null = null function makeLog(overrides: Partial & Pick): SyncLogData { @@ -373,6 +387,37 @@ afterEach(() => { }) describe('Connector credential reauthorization', () => { + it('expands each connection history independently through its labeled control', () => { + const container = renderSection(makeConnector(), [makeConnector({ id: 'connector-2' })]) + const controls = Array.from( + container.querySelectorAll('button[aria-label="Sync history"]') + ) + expect(controls).toHaveLength(2) + const historyId = controls[0].getAttribute('aria-controls') + expect(historyId).toBeTruthy() + expect(historyId).not.toBe(controls[1].getAttribute('aria-controls')) + + act(() => controls[0].click()) + + expect(controls[0].getAttribute('aria-expanded')).toBe('true') + expect(controls[1].getAttribute('aria-expanded')).toBe('false') + const history = document.getElementById(historyId!)! + const collapse = findButton(history, 'Hide history') + expect(collapse.getAttribute('aria-controls')).toBe(historyId) + expect(collapse.getAttribute('aria-expanded')).toBe('true') + act(() => { + collapse.focus() + collapse.click() + }) + expect(document.getElementById(historyId!)).toBeNull() + expect(controls[0].getAttribute('aria-expanded')).toBe('false') + expect(controls[1].getAttribute('aria-expanded')).toBe('false') + expect(document.activeElement).toBe( + container.querySelector('button[aria-label="Connection actions"]') + ) + expect(lifecycle.sync.mutate).not.toHaveBeenCalled() + }) + it('distinguishes configured sites and spaces without exposing credential fields', () => { const container = renderSection( makeConnector({ @@ -392,19 +437,45 @@ describe('Connector credential reauthorization', () => { expect(container.textContent).not.toContain('private-token') }) - it('fails closed when the connector credential cannot be resolved', () => { - const container = renderSection(makeConnector()) - const reconnectButton = Array.from(container.querySelectorAll('button')).find( - (button) => button.textContent === 'Reconnect' + it('routes an unavailable credential to settings without starting OAuth', () => { + const onEdit = vi.fn() + const container = renderComponent( + ) + act(() => findButton(container, 'Settings').click()) + expect(onEdit).toHaveBeenCalledOnce() + expect(connectOAuthModalMock).not.toHaveBeenCalled() + }) - expect(reconnectButton?.disabled).toBe(true) - - act(() => reconnectButton?.click()) - + it('waits for credential loading before deciding how to recover', () => { + oauthCredentialsState.isFetching = true + const container = renderSection(makeConnector()) + expect(findButton(container, 'Reconnect')).toBeDisabled() expect(connectOAuthModalMock).not.toHaveBeenCalled() }) + it.each([true, false])('renders loading and empty states with canEdit=%s', (canEdit) => { + const renderEmpty = (isLoading: boolean) => ( + + ) + const container = renderComponent(renderEmpty(true)) + expect(container.textContent).toContain('Loading connections…') + act(() => root?.render(renderEmpty(false))) + expect(container.textContent).toContain('No connected sources yet.') + }) + it('reauthorizes with the resolved credential provider and identity', () => { oauthCredentialsState.current = [ { id: 'credential-1', name: 'Workspace Slack', provider: 'slack-custom' }, @@ -515,6 +586,15 @@ describe('Connector credential reauthorization', () => { expect(connectOAuthModalMock).not.toHaveBeenCalled() }) + it('keeps successful-sync notices available when history is opened', () => { + const notice = 'Deletion reconciliation deferred until the next complete listing.' + const container = renderSection(makeConnector({ status: 'active', lastSyncError: notice })) + expect(container.textContent).not.toContain(notice) + act(() => findButton(container, 'Sync history').click()) + expect(container.textContent).toContain(notice) + expect(lifecycle.sync.mutate).not.toHaveBeenCalled() + }) + it('preserves pending OAuth return context when the recovery modal closes', () => { oauthCredentialsState.current = [ { id: 'credential-1', name: 'Workspace Slack', provider: 'slack-custom' }, @@ -741,32 +821,25 @@ describe('Connector credential reauthorization', () => { }) describe('shared connector lifecycle actions', () => { - it('distinguishes an incremental sync from a supported full resync', () => { - const container = renderComponent( - - ) - act(() => findButton(container, 'Sync now').click()) - expect(lifecycle.sync.mutate).toHaveBeenLastCalledWith({ - knowledgeBaseId: 'knowledge-1', - connectorId: 'connector-1', - rehydrate: false, - }) - lifecycle.sync.mutate.mockClear() - act(() => findButton(container, 'Full resync').click()) - expect(lifecycle.sync.mutate).not.toHaveBeenCalled() - const dialog = container.querySelector('[role="dialog"]')! - act(() => findButton(dialog, 'Full resync').click()) - expect(lifecycle.sync.mutate).toHaveBeenLastCalledWith( - { knowledgeBaseId: 'knowledge-1', connectorId: 'connector-1', rehydrate: true }, - expect.objectContaining({ onSuccess: expect.any(Function) }) - ) - act(() => lifecycle.sync.mutate.mock.lastCall![1].onSuccess()) - expect(container.querySelector('[role="dialog"]')).toBeNull() - }) + it.each(['confluence', 'databricks', 'github', 'gitlab'])( + 'offers only normal sync for %s', + (connectorType) => { + const container = renderComponent( + + ) + expect(container.textContent).not.toContain('Full resync') + act(() => findButton(container, 'Sync now').click()) + expect(lifecycle.sync.mutate).toHaveBeenCalledExactlyOnceWith({ + knowledgeBaseId: 'knowledge-1', + connectorId: 'connector-1', + }) + expect(container.querySelector('[role="dialog"]')).toBeNull() + } + ) it.each(['pending', 'running', 'disabled'] as const)( 'does not offer content resync or dispatch work while the member engine is %s', @@ -793,20 +866,6 @@ describe('shared connector lifecycle actions', () => { } ) - it('cancels a full resync without dispatching work', () => { - const container = renderComponent( - - ) - act(() => findButton(container, 'Full resync').click()) - act(() => findButton(container.querySelector('[role="dialog"]')!, 'Cancel').click()) - expect(lifecycle.sync.mutate).not.toHaveBeenCalled() - expect(container.querySelector('[role="dialog"]')).toBeNull() - }) - it.each([ { status: 'syncing', accessMode: 'admin', memberSyncStatus: 'idle', blocked: true }, { status: 'active', accessMode: 'members', memberSyncStatus: 'running', blocked: true }, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 8b8d5c4e68a..b2848f37e64 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -1,18 +1,24 @@ 'use client' -import { useState } from 'react' -import { Badge, Chip, cn, OverflowText, Tooltip } from '@sim/emcn' -import { ChevronDown, ChevronUp, CircleAlert, Loader, Users } from '@sim/emcn/icons' +import { useId, useRef, useState } from 'react' +import { Badge, Chip, cn } from '@sim/emcn' +import { Loader, Users } from '@sim/emcn/icons' import { format, formatDistanceToNow, isPast } from 'date-fns' import type { ConnectorData } from '@/lib/api/contracts/knowledge/connectors' import { type ResourceScope, resourceScopeFromOwner } from '@/lib/core/resource-scope' import { describeSearchSource } from '@/lib/sim-search/source-identity' import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase' -import { ConnectorActions } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions' +import { + ConnectorActionFeedback, + ConnectorActions, +} from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions' import { ConnectorRecovery } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-recovery' import { ConnectorSyncHistory } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-sync-history' import { getConnectorSyncState } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-sync-state' +import { useConnectorActions } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions' import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal' +import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' interface ConnectorsSectionProps { @@ -47,18 +53,15 @@ export function ConnectorsSection({ }: ConnectorsSectionProps) { const scope = explicitScope ?? resourceScopeFromOwner({ workspaceId }) const [editingConnector, setEditingConnector] = useState(null) - if (connectors.length === 0 && !canEdit && !isLoading) return null return (
{isLoading ? ( -
+ Loading connections… ) : connectors.length === 0 ? ( -

- No connected sources yet. Connect an external source to automatically sync documents. -

+ No connected sources yet. ) : ( -
+
{connectors.map((connector) => ( (null) + const actions = useConnectorActions({ connector, knowledgeBaseId, canEdit, onEdit }) + const historyId = useId() const [expanded, setExpanded] = useState(false) const connectorDef = CONNECTOR_META_REGISTRY[connector.connectorType] const sourceDescription = connectorDef @@ -113,101 +119,58 @@ function ConnectorCard({ getConnectorSyncState(connector) const statusConfig = STATUS_CONFIG[effectiveStatus as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.active + const syncDetails = [ + lastSyncAt && `Last sync: ${format(new Date(lastSyncAt), 'MMM d, h:mm a')}`, + !syncsPerMember && connector.lastSyncDocCount !== null && `${connector.lastSyncDocCount} docs`, + nextSyncAt && + connector.status === 'active' && + !syncInFlight && + `Next sync: ${ + isPast(new Date(nextSyncAt)) + ? 'pending' + : formatDistanceToNow(new Date(nextSyncAt), { addSuffix: true }) + }`, + ] + .filter(Boolean) + .join(' · ') return ( -
-
-
- {Icon && } -
-
- - - {syncInFlight && } - - - {statusConfig.label} +
+ } + iconVariant='custom' + title={connectorDef?.name || connector.connectorType} + description={sourceDescription || undefined} + badge={ + <> + + {statusConfig.label} + + {syncsPerMember && ( + + Per member - {syncsPerMember && ( - - Per member - - )} -
- {sourceDescription && ( - - - )} -
- {lastSyncAt && ( - Last sync: {format(new Date(lastSyncAt), 'MMM d, h:mm a')} - )} - {!syncsPerMember && connector.lastSyncDocCount !== null && ( - <> - · - {connector.lastSyncDocCount} docs - - )} - {nextSyncAt && connector.status === 'active' && !syncInFlight && ( - <> - · - - Next sync:{' '} - {isPast(new Date(nextSyncAt)) - ? 'pending' - : formatDistanceToNow(new Date(nextSyncAt), { addSuffix: true })} - - - )} - {lastSyncError && ( - - - - - {lastSyncError} - - )} - {connector.accessRewritePending && ( - <> - · - - - Updating access - - - )} -
-
-
- -
+ + } + trailing={ setExpanded((previous) => !previous), + }} /> - - - setExpanded((prev) => !prev)} - aria-label={expanded ? 'Hide history' : 'Sync history'} - aria-expanded={expanded} - leftIcon={expanded ? ChevronUp : ChevronDown} - /> - - {expanded ? 'Hide history' : 'Sync history'} - -
-
+ } + /> + {connector.accessRewritePending && ( + + + Updating access + + )} + {expanded && ( -
+
+
+ {syncDetails && ( +

{syncDetails}

+ )} + { + setExpanded(false) + actionsTriggerRef.current?.focus() + }} + > + Hide history + +
+ {lastSyncError && }
)} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts index 7c8f45295c2..9fbbcda5690 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions.ts @@ -33,7 +33,6 @@ export function useConnectorActions({ const update = useUpdateConnector() const remove = useDeleteConnector() const [confirmRemove, setConfirmRemove] = useState(false) - const [confirmFullResync, setConfirmFullResync] = useState(false) const [deleteDocuments, setDeleteDocuments] = useState(false) const requiresDocumentDeletion = connector.accessMode !== 'workspace' const state = getConnectorSyncState(connector) @@ -49,13 +48,10 @@ export function useConnectorActions({ remove.reset() } - function triggerSync(rehydrate = false) { - if (!canEdit || actionsDisabled || state.syncDisabled || (rehydrate && !state.canFullResync)) - return + function triggerSync() { + if (!canEdit || actionsDisabled || state.syncDisabled) return resetErrors() - const input = { knowledgeBaseId, connectorId: connector.id, rehydrate } - if (rehydrate) sync.mutate(input, { onSuccess: () => setConfirmFullResync(false) }) - else sync.mutate(input) + sync.mutate({ knowledgeBaseId, connectorId: connector.id }) } function setRemoveOpen(open: boolean) { @@ -74,20 +70,6 @@ export function useConnectorActions({ tooltip: state.syncTooltip, onSelect: () => triggerSync(), }, - ...(state.canFullResync - ? [ - { - id: 'full-resync', - text: 'Full resync', - disabled: state.syncDisabled || actionsDisabled, - onSelect: () => { - if (!canEdit || actionsDisabled || state.syncDisabled) return - resetErrors() - setConfirmFullResync(true) - }, - }, - ] - : []), ...(onEdit ? [{ id: 'settings', text: 'Settings', disabled: actionsDisabled, onSelect: onEdit }] : []), @@ -123,19 +105,7 @@ export function useConnectorActions({ actions, actionsDisabled, canEdit, - error: (confirmFullResync ? null : sync.error) ?? update.error, - fullResync: { - open: confirmFullResync, - onOpenChange: (open: boolean) => { - if (sync.isPending) return - setConfirmFullResync(open) - if (!open) sync.reset() - }, - pending: sync.isPending, - disabled: actionsDisabled || state.syncDisabled || !state.canFullResync, - error: sync.error, - onConfirm: () => triggerSync(true), - }, + error: sync.error ?? update.error, removal: { open: confirmRemove, onOpenChange: setRemoveOpen, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.test.tsx index d748e7ff572..8405ec68aec 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.test.tsx @@ -24,22 +24,65 @@ vi.mock('@/hooks/queries/kb/knowledge', () => ({ useUpdateDocument: () => ({ ...mocks.retryState, mutate: mocks.retry }), })) +vi.mock( + '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form', + () => ({ + useConnectorSettingsForm: () => ({ + displayName: 'GitHub', + saving: false, + canSave: true, + save: vi.fn(), + fieldsProps: {}, + }), + }) +) +vi.mock( + '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields', + () => ({ ConnectorSettingsFields: () =>
Settings content
}) +) + +import type { ConnectorData } from '@/lib/api/contracts/knowledge/connectors' import { ConnectorDocuments } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents' -import { ConnectorDocumentsTab } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab' +import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal' + +const connector: ConnectorData = { + id: 'connector', + knowledgeBaseId: 'kb', + connectorType: 'github', + credentialId: null, + sourceConfig: {}, + syncMode: 'full', + syncIntervalMinutes: 60, + status: 'active', + lastSyncAt: null, + lastSyncError: null, + lastSyncDocCount: null, + nextSyncAt: null, + consecutiveFailures: 0, + accessMode: 'workspace', + viewerMembership: null, + credentialGroupId: null, + credentialGroupOptionId: null, + memberSyncStatus: 'idle', + lastMemberSyncAt: null, + nextMemberSyncAt: null, + lastMemberSyncError: null, + memberSyncConsecutiveFailures: 0, + accessRewritePending: false, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', +} let root: Root -function render() { +function render(showDocuments = true) { root = createRoot(document.createElement('div')) rerender() + if (showDocuments) act(() => button('Documents').click()) } function rerender() { act(() => root.render( - - - - - + ) ) } @@ -118,6 +161,31 @@ afterEach(() => { }) describe('connector document recovery', () => { + it('keeps focused navigation mounted and document filters selected across tabs', () => { + render(false) + expect(mocks.query).not.toHaveBeenCalled() + const settings = button('Settings') + const documents = button('Documents') + act(() => { + documents.focus() + documents.click() + }) + expect(document.activeElement).toBe(documents) + selectStatus('Failed (1)') + act(() => { + settings.focus() + settings.click() + }) + expect(button('Settings')).toBe(settings) + expect(button('Documents')).toBe(documents) + expect(document.activeElement).toBe(settings) + expect(document.querySelector('[aria-label="Document status"]')).toBeNull() + act(() => documents.click()) + expect(document.querySelector('[aria-label="Document status"]')?.textContent).toContain( + 'Failed (1)' + ) + }) + it('labels Search documents by the current viewer access without changing general knowledge bases', () => { render() expect(document.body.textContent).not.toContain('Documents you can access') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.tsx index da2b7a5188d..6acf63c4d06 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab.tsx @@ -1,28 +1,49 @@ 'use client' -import { useState } from 'react' -import { ChipModalField } from '@sim/emcn' import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors' +import { ConnectorDocumentStatusFilter } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter' import { ConnectorDocuments } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents' +import { useConnectorDocuments } from '@/hooks/queries/kb/connectors' interface ConnectorDocumentsTabProps { knowledgeBaseId: string connectorId: string + filter: ConnectorDocumentFilter + onFilterChange: (filter: ConnectorDocumentFilter) => void } export function ConnectorDocumentsTab({ knowledgeBaseId, connectorId, + filter, + onFilterChange, }: ConnectorDocumentsTabProps) { - const [filter, setFilter] = useState('active') return ( - +
- +
+ ) +} + +export function ConnectorDocumentsTabFilter({ + knowledgeBaseId, + connectorId, + filter, + onFilterChange, +}: ConnectorDocumentsTabProps) { + const query = useConnectorDocuments(knowledgeBaseId, connectorId, { filter }) + return ( + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx index 1ca946f2895..298ba33260d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx @@ -8,8 +8,12 @@ import { ChipModalHeader, ChipModalTabs, } from '@sim/emcn' +import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors' import type { ResourceScope } from '@/lib/core/resource-scope' -import { ConnectorDocumentsTab } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab' +import { + ConnectorDocumentsTab, + ConnectorDocumentsTabFilter, +} from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-documents-tab' import { ConnectorSettingsFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields' import { useConnectorSettingsForm } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form' import { withBrandIcon } from '@/blocks/brand-icon' @@ -33,6 +37,7 @@ export function EditConnectorModal({ scope, }: EditConnectorModalProps) { const [activeTab, setActiveTab] = useState('settings') + const [documentFilter, setDocumentFilter] = useState('active') const form = useConnectorSettingsForm({ scope, knowledgeBaseId, @@ -56,19 +61,34 @@ export function EditConnectorModal({ Edit {form.displayName} - +
+ + {activeTab === 'documents' && ( + + )} +
{activeTab === 'settings' ? ( ) : ( - + )}
{activeTab === 'settings' && ( diff --git a/apps/sim/hooks/queries/kb/connectors.ts b/apps/sim/hooks/queries/kb/connectors.ts index f0c284635f2..e7f89997665 100644 --- a/apps/sim/hooks/queries/kb/connectors.ts +++ b/apps/sim/hooks/queries/kb/connectors.ts @@ -757,18 +757,12 @@ export function useDeleteConnector() { interface TriggerSyncParams { knowledgeBaseId: string connectorId: string - /** Force re-hydration + re-index of rendered content (the "Full resync" action). */ - rehydrate?: boolean } -async function triggerSync({ - knowledgeBaseId, - connectorId, - rehydrate, -}: TriggerSyncParams): Promise { +async function triggerSync({ knowledgeBaseId, connectorId }: TriggerSyncParams): Promise { await requestJson(triggerKnowledgeConnectorSyncContract, { params: { id: knowledgeBaseId, connectorId }, - query: rehydrate ? { rehydrate: true } : {}, + query: {}, }) } diff --git a/packages/emcn/src/components/chip-modal/chip-modal.tsx b/packages/emcn/src/components/chip-modal/chip-modal.tsx index b714d85f060..87f1b872abf 100644 --- a/packages/emcn/src/components/chip-modal/chip-modal.tsx +++ b/packages/emcn/src/components/chip-modal/chip-modal.tsx @@ -387,7 +387,7 @@ function ChipModalTabs({ onChange={onChange} aria-label={ariaLabel} options={tabs.map((tab) => ({ value: tab.value, label: tab.label, icon: tab.icon }))} - className={className} + className={cn('shrink-0', className)} /> ) } From 8e2623d5a60d56682218bedf691340943f253519 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 11 Sep 2026 16:09:11 -0700 Subject: [PATCH 02/15] fix(search): bound retrieval and progressively read document evidence (#7789) * fix(search): bound retrieval and progressively read document evidence * fix(search): protect query output and validate document continuation --- .../knowledge/search/route.provenance.test.ts | 5 +- .../[knowledgeBaseId]/[documentId]/page.tsx | 29 +- .../[documentId]/search-params.ts | 25 + .../lib/api/contracts/knowledge/documents.ts | 19 +- .../lib/copilot/generated/tool-catalog-v1.ts | 25 +- .../lib/copilot/generated/tool-schemas-v1.ts | 25 +- .../copilot/generated/trace-attributes-v1.ts | 2 + .../server/knowledge/workspace-search.test.ts | 38 +- .../server/knowledge/workspace-search.ts | 184 +++--- .../gitlab-live.integration.ts | 3 +- .../search-latency.integration.ts | 286 ++++++++- .../application/read-search-document.test.ts | 97 ++- .../application/read-search-document.ts | 110 +++- .../lib/knowledge/application/search.test.ts | 5 +- apps/sim/lib/knowledge/application/search.ts | 22 +- .../workspace-search.activity.test.ts | 1 + .../knowledge/application/workspace-search.ts | 22 +- apps/sim/lib/knowledge/chunks/service.ts | 10 +- apps/sim/lib/knowledge/chunks/types.ts | 2 + apps/sim/lib/knowledge/search/budget.test.ts | 55 ++ apps/sim/lib/knowledge/search/budget.ts | 115 ++++ apps/sim/lib/knowledge/search/diagnostics.ts | 9 +- apps/sim/lib/knowledge/search/queries.test.ts | 2 +- apps/sim/lib/knowledge/search/queries.ts | 562 +++++++++--------- apps/sim/lib/knowledge/search/snippet.test.ts | 29 + apps/sim/lib/knowledge/search/snippet.ts | 39 ++ 26 files changed, 1296 insertions(+), 425 deletions(-) create mode 100644 apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/search-params.ts create mode 100644 apps/sim/lib/knowledge/search/budget.test.ts create mode 100644 apps/sim/lib/knowledge/search/budget.ts diff --git a/apps/sim/app/api/v2/knowledge/search/route.provenance.test.ts b/apps/sim/app/api/v2/knowledge/search/route.provenance.test.ts index 3971a4c4ca5..7e847cb2797 100644 --- a/apps/sim/app/api/v2/knowledge/search/route.provenance.test.ts +++ b/apps/sim/app/api/v2/knowledge/search/route.provenance.test.ts @@ -59,7 +59,10 @@ vi.mock('@/lib/knowledge/embeddings', () => ({ vi.mock('@/lib/knowledge/search/queries', () => ({ generateSearchEmbedding: mocks.generateEmbedding, - executeKnowledgeSearch: mocks.executeSearch, + retrieveKnowledgeSearch: async (...args: unknown[]) => ({ + rows: await mocks.executeSearch(...args), + retrieval: { status: 'complete', timedOutLegs: [] }, + }), getDocumentMetadataByIds: mocks.getDocumentMetadata, })) diff --git a/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/page.tsx b/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/page.tsx index ebabd37f6fc..6cd806b52bc 100644 --- a/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/page.tsx +++ b/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/page.tsx @@ -1,16 +1,21 @@ import { ChipLink } from '@sim/emcn' import { notFound, redirect } from 'next/navigation' +import type { SearchParams } from 'nuqs/server' import { readSearchDocumentResultSchema } from '@/lib/api/contracts/knowledge/documents' import { getSession } from '@/lib/auth' import { OrchestrationError } from '@/lib/core/orchestration/types' import { readSearchDocument } from '@/lib/knowledge/application/read-search-document' import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' +import { + loadDocumentReadParams, + serializeDocumentReadParams, +} from '@/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/search-params' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' interface OrganizationDocumentPageProps { params: Promise<{ organizationId: string; knowledgeBaseId: string; documentId: string }> - searchParams: Promise<{ offset?: string }> + searchParams: Promise } export default async function OrganizationDocumentPage({ @@ -18,15 +23,15 @@ export default async function OrganizationDocumentPage({ searchParams, }: OrganizationDocumentPageProps) { const { organizationId, knowledgeBaseId, documentId } = await params - const { offset: rawOffset } = await searchParams - const offset = rawOffset === undefined ? 0 : Number(rawOffset) - if (!Number.isInteger(offset) || offset < 0 || offset > 5000) notFound() + const position = await loadDocumentReadParams(searchParams, { strict: true }).catch(() => + notFound() + ) const href = `/o/${encodeURIComponent(organizationId)}/knowledge/${encodeURIComponent(knowledgeBaseId)}/${encodeURIComponent(documentId)}` const session = await getSession() if (!session?.user) { redirect( buildAuthCrossLink('/login', { - callbackUrl: offset ? `${href}?offset=${offset}` : href, + callbackUrl: serializeDocumentReadParams(href, position), isInviteFlow: false, }) ) @@ -39,15 +44,15 @@ export default async function OrganizationDocumentPage({ input: { documentId, assertedOrganizationId: organizationId, - offset, - limit: 20, + ...position, + limit: 3, resultSecretRegistry: registry, }, }) } catch (error) { if ( error instanceof OrchestrationError && - (error.code === 'not_found' || error.code === 'forbidden') + (error.code === 'not_found' || error.code === 'forbidden' || error.code === 'validation') ) notFound() throw error @@ -71,11 +76,11 @@ export default async function OrganizationDocumentPage({

))} diff --git a/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/search-params.ts b/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/search-params.ts new file mode 100644 index 00000000000..42f9dd224e8 --- /dev/null +++ b/apps/sim/app/o/[organizationId]/knowledge/[knowledgeBaseId]/[documentId]/search-params.ts @@ -0,0 +1,25 @@ +import { createLoader, createParser, createSerializer } from 'nuqs/server' + +const parseAsDocumentPosition = createParser({ + parse: (value) => { + if (!/^\d+$/.test(value)) return null + const position = Number(value) + return Number.isSafeInteger(position) && position <= 2147483647 ? position : null + }, + serialize: String, +}).withDefault(0) + +export const documentReadParams = { + startChunkIndex: parseAsDocumentPosition, + startOffset: parseAsDocumentPosition, +} + +const documentReadUrlKeys = { + urlKeys: { + startChunkIndex: 'start-chunk-index', + startOffset: 'start-offset', + }, +} as const + +export const loadDocumentReadParams = createLoader(documentReadParams, documentReadUrlKeys) +export const serializeDocumentReadParams = createSerializer(documentReadParams, documentReadUrlKeys) diff --git a/apps/sim/lib/api/contracts/knowledge/documents.ts b/apps/sim/lib/api/contracts/knowledge/documents.ts index a5219138b05..4b586ff6765 100644 --- a/apps/sim/lib/api/contracts/knowledge/documents.ts +++ b/apps/sim/lib/api/contracts/knowledge/documents.ts @@ -429,8 +429,23 @@ export const readSearchDocumentResultSchema = z.object({ knowledgeBaseId: z.string().min(1), documentName: z.string().nullable(), sourceUrl: z.string().nullable(), - chunks: z.array(z.object({ content: z.string(), chunkIndex: z.number().int().min(0) })).max(50), + chunks: z + .array( + z.object({ + content: z.string().max(8000), + chunkIndex: z.number().int().min(0), + startOffset: z.number().int().min(0), + endOffset: z.number().int().min(0), + totalCharacters: z.number().int().min(0), + }) + ) + .max(8), hasMore: z.boolean(), - nextOffset: z.number().int().min(0).nullable(), + next: z + .object({ + startChunkIndex: z.number().int().min(0), + startOffset: z.number().int().min(0), + }) + .nullable(), }) export type ReadSearchDocumentResult = z.output diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index b65bebaf7d1..22fb32d4a10 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -4750,16 +4750,24 @@ export const ReadDocument: ToolCatalogEntry = { type: 'string', }, limit: { - default: 20, - description: 'Maximum number of chunks to read.', - maximum: 50, + default: 3, + description: + 'Maximum chunks to read; the server may return fewer to fit its text budget. Follow next when more context is needed.', + maximum: 8, minimum: 1, type: 'integer', }, - offset: { - default: 0, - description: 'Number of chunks to skip.', - maximum: 5000, + startChunkIndex: { + description: + "Inclusive chunk index from search or a previous read's next object. Gaps from disabled chunks are skipped.", + maximum: 2147483647, + minimum: 0, + type: 'integer', + }, + startOffset: { + description: + 'UTF-16 character offset within startChunkIndex. Omit to read the chunk from its start, or copy next.startOffset to continue a partial chunk.', + maximum: 2147483647, minimum: 0, type: 'integer', }, @@ -5616,7 +5624,8 @@ export const SearchWorkspace: ToolCatalogEntry = { }, topK: { default: 20, - description: 'Maximum number of matching chunks to return.', + description: + 'Maximum number of matching passage previews to return. Retrieval ranking is independent of preview length.', maximum: 50, minimum: 1, type: 'integer', diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 388d341ae1f..72e603a3613 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -4694,16 +4694,24 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: 'string', }, limit: { - default: 20, - description: 'Maximum number of chunks to read.', - maximum: 50, + default: 3, + description: + 'Maximum chunks to read; the server may return fewer to fit its text budget. Follow next when more context is needed.', + maximum: 8, minimum: 1, type: 'integer', }, - offset: { - default: 0, - description: 'Number of chunks to skip.', - maximum: 5000, + startChunkIndex: { + description: + "Inclusive chunk index from search or a previous read's next object. Gaps from disabled chunks are skipped.", + maximum: 2147483647, + minimum: 0, + type: 'integer', + }, + startOffset: { + description: + 'UTF-16 character offset within startChunkIndex. Omit to read the chunk from its start, or copy next.startOffset to continue a partial chunk.', + maximum: 2147483647, minimum: 0, type: 'integer', }, @@ -5518,7 +5526,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, topK: { default: 20, - description: 'Maximum number of matching chunks to return.', + description: + 'Maximum number of matching passage previews to return. Retrieval ranking is independent of preview length.', maximum: 50, minimum: 1, type: 'integer', diff --git a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts index fdc1a5da7ca..df4a808f809 100644 --- a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts @@ -478,6 +478,7 @@ export const TraceAttr = { LlmStreamChunks: 'llm.stream.chunks', LlmStreamFirstChunkBytes: 'llm.stream.first_chunk_bytes', LlmStreamFirstChunkMs: 'llm.stream.first_chunk_ms', + LlmStreamFirstTokenMs: 'llm.stream.first_token_ms', LlmStreamOpenMs: 'llm.stream.open_ms', LlmStreamTotalMs: 'llm.stream.total_ms', LockAcquired: 'lock.acquired', @@ -1159,6 +1160,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'llm.stream.chunks', 'llm.stream.first_chunk_bytes', 'llm.stream.first_chunk_ms', + 'llm.stream.first_token_ms', 'llm.stream.open_ms', 'llm.stream.total_ms', 'lock.acquired', diff --git a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts index 1e669a7b45b..b83a791fc06 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts @@ -35,9 +35,6 @@ vi.mock('@/lib/knowledge/application/read-search-document', () => ({ execute: mocks.read, }, })) -vi.mock('@/executor/utils/resolved-secret-content-projection', () => ({ - projectResolvedSecretModelContent: (value: unknown) => ({ safe: true, value }), -})) import { readDocumentServerTool, @@ -61,6 +58,7 @@ describe('Assistant retrieval tools', () => { beforeEach(() => { vi.clearAllMocks() mocks.search.mockResolvedValue({ + retrieval: { status: 'complete', timedOutLegs: [] }, knowledgeBases: [{ id: 'index', name: 'Enterprise Search' }], results: [ { @@ -83,7 +81,7 @@ describe('Assistant retrieval tools', () => { sourceUrl: 'https://source.test/doc', chunks: [{ content: 'body', chunkIndex: 0 }], hasMore: false, - nextOffset: null, + next: null, }) }) it('pins organization and private chat while reusing the canonical search index and citations', async () => { @@ -187,11 +185,29 @@ describe('Assistant retrieval tools', () => { }) ) }) + it('returns only the projected query to the model', async () => { + const secret = 'private-resolved-query-token' + const registry = new ResolvedSecretTraceRegistry([ + { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, + ]) + registry.recordResolved('TOKEN', secret) + const result = await searchWorkspaceServerTool.execute( + { query: `Find ${secret}` }, + { ...context, resolvedSecretTraceRegistry: registry } + ) + expect(result).toMatchObject({ success: true, data: { query: 'Find {{TOKEN}}' } }) + expect(mocks.search).toHaveBeenCalledWith( + expect.objectContaining({ input: expect.objectContaining({ query: 'Find {{TOKEN}}' }) }) + ) + expect(JSON.stringify(result)).not.toContain(secret) + }) + it.each([0, 20, 50])( 'measures UTF-8 bytes for %i passages without logging their content', async (count) => { const content = 'Confidential passage é🔎'.repeat(100) mocks.search.mockResolvedValueOnce({ + retrieval: { status: 'complete', timedOutLegs: [] }, knowledgeBases: [{ id: 'index', name: 'Enterprise Search' }], results: Array.from({ length: count }, (_, index) => ({ knowledgeBaseId: 'index', @@ -217,8 +233,9 @@ describe('Assistant retrieval tools', () => { expect.objectContaining({ toolCallId: 'call', toolResultBytes: Buffer.byteLength(JSON.stringify(output)), - passageBytes: count * Buffer.byteLength(content), - maxPassageBytes: count ? Buffer.byteLength(content) : 0, + passageBytes: count * Buffer.byteLength(content.slice(0, 1200)), + originalPassageBytes: count * Buffer.byteLength(content), + maxPassageBytes: count ? Buffer.byteLength(content.slice(0, 1200)) : 0, uniqueDocumentCount: Math.min(count, 4), }) ) @@ -245,6 +262,7 @@ describe('Assistant retrieval tools', () => { }) it('projects the provider name for connected-source citations instead of the index name', async () => { mocks.search.mockResolvedValueOnce({ + retrieval: { status: 'complete', timedOutLegs: [] }, knowledgeBases: [{ id: 'index', name: 'Sim Search' }], results: [ { @@ -292,20 +310,20 @@ describe('Assistant retrieval tools', () => { }) it('reads a selected document through the shared use case and rejects unbounded pages', async () => { expect( - await readDocumentServerTool.execute({ documentId: 'doc', offset: 20 }, context) + await readDocumentServerTool.execute({ documentId: 'doc', startChunkIndex: 20 }, context) ).toMatchObject({ success: true }) expect(mocks.read).toHaveBeenCalledWith( expect.objectContaining({ input: expect.objectContaining({ assertedWorkspaceId: 'workspace', filters: context.assistantSearch, - offset: 20, - limit: 20, + startChunkIndex: 20, + limit: 3, }), }) ) expect( - await readDocumentServerTool.execute({ documentId: 'doc', limit: 10000 }, context) + await readDocumentServerTool.execute({ documentId: 'doc', limit: 9 }, context) ).toMatchObject({ success: false }) expect(mocks.read).toHaveBeenCalledOnce() }) diff --git a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts index 01f6cb833f5..8b014c4d17c 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts @@ -15,6 +15,7 @@ import { searchWorkspaceKnowledge, } from '@/lib/knowledge/application/workspace-search' import { sourceAuthor } from '@/lib/knowledge/search/author' +import { SearchDeadlineError } from '@/lib/knowledge/search/budget' import { createKnowledgeDocumentCitation } from '@/lib/knowledge/search/citation' import { annotateSearchDiagnostics, @@ -23,6 +24,7 @@ import { withSearchDiagnostics, } from '@/lib/knowledge/search/diagnostics' import { intersectWorkspaceSearchFilters } from '@/lib/knowledge/search/filters' +import { matchPassage } from '@/lib/knowledge/search/snippet' import { connectorDisplayName } from '@/lib/sim-search/connectors' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' @@ -33,8 +35,9 @@ const searchInputSchema = workspaceSearchFiltersSchema.extend({ }) const readInputSchema = z.object({ documentId: z.string().min(1).max(200), - offset: z.number().int().min(0).max(5000).default(0), - limit: z.number().int().min(1).max(50).default(20), + limit: z.number().int().min(1).max(8).default(3), + startChunkIndex: z.number().int().min(0).max(2147483647).optional(), + startOffset: z.number().int().min(0).max(2147483647).optional(), }) const CITATION_INSTRUCTION = @@ -46,6 +49,7 @@ export const searchWorkspaceServerTool: BaseServerTool = { return withSearchDiagnostics( { surface: context?.searchSurface ?? 'copilot', + operation: 'search_workspace', toolCallId: context?.toolCallId, executionId: context?.executionId, }, @@ -63,9 +67,11 @@ export const searchWorkspaceServerTool: BaseServerTool = { message: 'Search query contains protected content. Rephrase the query.', } } + const safeQuery = projected.value const input = { - query: projected.value, + query: safeQuery, topK, + allowPartialResults: true, filters: intersectWorkspaceSearchFilters(requestedFilters, context?.assistantSearch), surface: context?.searchSurface ?? 'copilot', resultSecretRegistry: registry, @@ -87,38 +93,48 @@ export const searchWorkspaceServerTool: BaseServerTool = { const names = new Map(result.knowledgeBases.map((base) => [base.id, base.name])) const output = { success: true, - message: `Found ${result.results.length} passages. ${CITATION_INSTRUCTION}`, + message: `${result.retrieval.status === 'partial' ? 'Partial search: a retrieval branch reached its deadline. These results cannot establish absence or completeness. ' : ''}Found ${result.results.length} passage previews. Read a document at its chunkIndex for more context. ${CITATION_INSTRUCTION}`, data: { - query, - results: result.results.map((item) => ({ - documentId: item.documentId, - knowledgeBaseId: item.knowledgeBaseId, - knowledgeBaseName: names.get(item.knowledgeBaseId) ?? '', - siteName: item.connectorType - ? connectorDisplayName(item.connectorType) - : names.get(item.knowledgeBaseId), - documentName: item.documentName, - sourceUrl: item.sourceUrl, - connectorType: item.connectorType, - sourceModifiedAt: item.sourceModifiedAt?.toISOString() ?? null, - author: sourceAuthor(item.metadata), - content: item.content, - chunkIndex: item.chunkIndex, - similarity: item.similarity, - ...createKnowledgeDocumentCitation({ - scope, - knowledgeBaseId: item.knowledgeBaseId, + query: safeQuery, + retrieval: result.retrieval, + results: result.results.map((item) => { + const content = projectResolvedSecretModelContent(item.content, registry) + if (!content.safe || typeof content.value !== 'string') + throw new Error('Knowledge result provenance is unavailable') + return { documentId: item.documentId, + knowledgeBaseId: item.knowledgeBaseId, + knowledgeBaseName: names.get(item.knowledgeBaseId) ?? '', + siteName: item.connectorType + ? connectorDisplayName(item.connectorType) + : names.get(item.knowledgeBaseId), + documentName: item.documentName, sourceUrl: item.sourceUrl, - baseUrl: getBaseUrl(), - }), - })), + connectorType: item.connectorType, + sourceModifiedAt: item.sourceModifiedAt?.toISOString() ?? null, + author: sourceAuthor(item.metadata), + ...matchPassage(content.value, safeQuery, 1200), + chunkIndex: item.chunkIndex, + similarity: item.similarity, + ...createKnowledgeDocumentCitation({ + scope, + knowledgeBaseId: item.knowledgeBaseId, + documentId: item.documentId, + sourceUrl: item.sourceUrl, + baseUrl: getBaseUrl(), + }), + } + }), }, } const passageBytes = output.data.results.map((item) => Buffer.byteLength(item.content)) annotateSearchDiagnostics({ toolResultBytes: Buffer.byteLength(JSON.stringify(output)), passageBytes: passageBytes.reduce((total, bytes) => total + bytes, 0), + originalPassageBytes: result.results.reduce( + (total, item) => total + Buffer.byteLength(item.content), + 0 + ), maxPassageBytes: Math.max(0, ...passageBytes), uniqueDocumentCount: new Set(output.data.results.map((item) => item.documentId)).size, }) @@ -128,10 +144,13 @@ export const searchWorkspaceServerTool: BaseServerTool = { logger.error('Workspace search failed', { error }) return { success: false, + retryable: error instanceof SearchDeadlineError, message: - error instanceof z.ZodError - ? 'Invalid search arguments' - : messageForCopilotKnowledgeError(error), + error instanceof SearchDeadlineError + ? error.message + : error instanceof z.ZodError + ? 'Invalid search arguments' + : messageForCopilotKnowledgeError(error), } } } @@ -142,50 +161,69 @@ export const searchWorkspaceServerTool: BaseServerTool = { export const readDocumentServerTool: BaseServerTool = { name: 'read_document', async execute(raw, context?: ServerToolContext) { - try { - const scope = requireCopilotKnowledgeScope(context) - const input = readInputSchema.parse(raw) - const registry = context?.resolvedSecretTraceRegistry - if (!registry) throw new Error('Knowledge result provenance is unavailable') - const readInput = { - ...input, - ...(scope.kind === 'organization' - ? { assertedOrganizationId: scope.organizationId } - : { assertedWorkspaceId: scope.workspaceId }), - filters: intersectWorkspaceSearchFilters( - { documentIds: [input.documentId] }, - context?.assistantSearch - ), - resultSecretRegistry: registry, - signal: context?.abortSignal, - } - const result = - scope.kind === 'organization' - ? await executeCopilotOrganizationKnowledgeUseCase(context, readSearchDocument, readInput) - : await executeCopilotKnowledgeUseCase(context, readSearchDocument, readInput) - return { - success: true, - message: CITATION_INSTRUCTION, - data: { - ...result, - ...createKnowledgeDocumentCitation({ - scope, - knowledgeBaseId: result.knowledgeBaseId, - documentId: result.documentId, - sourceUrl: result.sourceUrl, - baseUrl: getBaseUrl(), - }), - }, - } - } catch (error) { - logger.error('Document read failed', { error }) - return { - success: false, - message: - error instanceof z.ZodError - ? 'Invalid document arguments' - : messageForCopilotKnowledgeError(error), + return withSearchDiagnostics( + { + surface: context?.searchSurface ?? 'copilot', + toolCallId: context?.toolCallId, + executionId: context?.executionId, + operation: 'read_document', + }, + async () => { + try { + const scope = requireCopilotKnowledgeScope(context) + const input = readInputSchema.parse(raw) + const registry = context?.resolvedSecretTraceRegistry + if (!registry) throw new Error('Knowledge result provenance is unavailable') + const readInput = { + ...input, + ...(scope.kind === 'organization' + ? { assertedOrganizationId: scope.organizationId } + : { assertedWorkspaceId: scope.workspaceId }), + filters: intersectWorkspaceSearchFilters( + { documentIds: [input.documentId] }, + context?.assistantSearch + ), + resultSecretRegistry: registry, + signal: context?.abortSignal, + } + const result = await measureSearchStage('document_read', () => + scope.kind === 'organization' + ? executeCopilotOrganizationKnowledgeUseCase(context, readSearchDocument, readInput) + : executeCopilotKnowledgeUseCase(context, readSearchDocument, readInput) + ) + const output = { + success: true, + message: CITATION_INSTRUCTION, + data: { + ...result, + ...createKnowledgeDocumentCitation({ + scope, + knowledgeBaseId: result.knowledgeBaseId, + documentId: result.documentId, + sourceUrl: result.sourceUrl, + baseUrl: getBaseUrl(), + }), + }, + } + annotateSearchDiagnostics({ + toolResultBytes: Buffer.byteLength(JSON.stringify(output)), + passageBytes: result.chunks.reduce( + (total, chunk) => total + Buffer.byteLength(chunk.content), + 0 + ), + }) + return output + } catch (error) { + logger.error('Document read failed', { error }) + return { + success: false, + message: + error instanceof z.ZodError + ? 'Invalid document arguments' + : messageForCopilotKnowledgeError(error), + } + } } - } + ) }, } diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts index 0ffcd293d9f..e515978e548 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts @@ -829,8 +829,7 @@ describe.skipIf(!fixtureFile)('live self-hosted GitLab ingestion and permission input: { documentId: ordinary.id, assertedOrganizationId: ids.organizationId, - offset: 0, - limit: 10, + limit: 3, resultSecretRegistry: new ResolvedSecretTraceRegistry(), }, }) diff --git a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts index 5646bd707d1..b1792cee86d 100644 --- a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts @@ -6,6 +6,7 @@ import { credential, credentialGroup, document, + embedding, knowledgeBase, knowledgeConnector, member, @@ -18,13 +19,27 @@ import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, type MockInstance, vi } from 'vitest' import { z } from 'zod' -import { searchWorkspaceServerTool } from '@/lib/copilot/tools/server/knowledge/workspace-search' +import type { + MothershipStreamV1CheckpointPausePayload, + MothershipStreamV1ToolCallDescriptor, +} from '@/lib/copilot/generated/mothership-stream-v1' +import { isContractStreamEventEnvelope } from '@/lib/copilot/request/session/contract' +import { + readDocumentServerTool, + searchWorkspaceServerTool, +} from '@/lib/copilot/tools/server/knowledge/workspace-search' import { seedSearchReaderFixture } from '@/lib/knowledge/__integration__/seed-search-reader-fixture' import { createKnowledgeAclFixtureIds, seedKnowledgeAclFixture, } from '@/lib/knowledge/__integration__/seed-source-access-fixture' import { searchScopedKnowledge } from '@/lib/knowledge/application/workspace-search' +import { + SearchBudget, + SearchDeadlineError, + type SearchExecutor, +} from '@/lib/knowledge/search/budget' +import type { SearchStage } from '@/lib/knowledge/search/diagnostics' import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' @@ -39,6 +54,7 @@ vi.hoisted(() => { } }) +const externalFetch = globalThis.fetch const enabled = process.env.KNOWLEDGE_SEARCH_PERFORMANCE_TEST === 'true' const chunkCount = Number(process.env.KNOWLEDGE_SEARCH_PERFORMANCE_CHUNKS ?? 20_000) const dimensions = 1536 @@ -400,7 +416,106 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu await db.$client.end() }, 120_000) + it('cancels slow SQL on the server and restores pooled connection settings', async () => { + const budget = new SearchBudget('keyword', performance.now() + 200) + const started = performance.now() + await expect( + budget.query('keyword.sql', (tx) => tx.execute(sql`SELECT pg_sleep(5)`)) + ).rejects.toBeInstanceOf(SearchDeadlineError) + expect(performance.now() - started).toBeLessThan(2000) + const [active] = await db.execute<{ count: number }>( + sql`SELECT count(*)::int AS count FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND state = 'active' AND query = 'SELECT pg_sleep(5)'` + ) + expect(active.count).toBe(0) + const [settings] = await db.execute<{ timeout: string }>( + sql`SELECT current_setting('statement_timeout') AS timeout` + ) + expect(settings.timeout).toBe('0') + }) + + it('expires waiting for a saturated pool without executing abandoned work', async () => { + let release!: () => void + const released = new Promise((resolve) => { + release = resolve + }) + let markSaturated!: () => void + const saturated = new Promise((resolve) => { + markSaturated = resolve + }) + let acquired = 0 + const holders = Array.from({ length: db.$client.options.max }, () => + db.transaction(async () => { + if (++acquired === db.$client.options.max) markSaturated() + await released + }) + ) + const run = vi.fn((tx: SearchExecutor) => tx.execute(sql`SELECT 1`)) + const started = performance.now() + try { + await saturated + const budget = new SearchBudget('keyword', performance.now() + 200) + await expect(budget.query('keyword.sql', run)).rejects.toBeInstanceOf(SearchDeadlineError) + expect(performance.now() - started).toBeLessThan(2000) + } finally { + release() + await Promise.all(holders) + } + await db.execute(sql`SELECT 1`) + expect(run).not.toHaveBeenCalled() + }) + + it.each(['keyword', 'both'] as const)( + 'handles %s SQL branches exceeding the deadline explicitly', + async (delayedLegs) => { + const query = SearchBudget.prototype.query + const delayed = vi.spyOn(SearchBudget.prototype, 'query').mockImplementation(function ( + this: SearchBudget, + stage: SearchStage, + run: (executor: SearchExecutor) => PromiseLike + ): Promise { + return query.call(this, stage, async (tx) => { + if (delayedLegs === 'both' || this.leg === 'keyword') + await tx.execute(sql`SELECT pg_sleep(9)`) + return run(tx) + }) as Promise + }) + try { + const result = await searchWorkspaceServerTool.execute( + { query: 'Orion deployment', topK: 15 }, + { + userId: ids.aliceId, + workspaceId: ids.workspaceId, + toolCallId: generateId(), + copilotToolExecution: true, + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry([], { + userId: ids.aliceId, + workspaceId: ids.workspaceId, + }), + } + ) + if (delayedLegs === 'both') { + expect(result).toMatchObject({ success: false, retryable: true }) + return + } + expect(result).toMatchObject({ + success: true, + data: { retrieval: { status: 'partial', timedOutLegs: ['keyword'] } }, + }) + const parsed = resultSchema.parse(result) + expect(parsed.data.results.length).toBeGreaterThan(0) + expect( + parsed.data.results.every((row) => row.knowledgeBaseId === ids.knowledgeBaseId) + ).toBe(true) + report['deadline.partial'] = { resultCount: parsed.data.results.length } + } finally { + delayed.mockRestore() + } + }, + 30_000 + ) + it('records first and repeated application searches with the actual SQL plans', async () => { + const before = embeddingCalls for (let iteration = 0; iteration < 2; iteration++) { const { result, plans } = await sample(`broad.${iteration}`, () => search()) expect(result.data.results).toHaveLength(15) @@ -412,7 +527,7 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu expect(vectorPlans).toHaveLength(1) expect(usesVectorIndex(vectorPlans[0].plan[0].Plan)).toBe(true) } - expect(embeddingCalls).toBe(2) + expect(embeddingCalls - before).toBe(2) }, 180_000) it('compares the Search tab and Assistant with the same person, query and index', async () => { @@ -555,4 +670,171 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu true ) }, 180_000) + /** Opt in with local Sim and Go URLs; uses the real configured provider, billing adapter, and async resume protocol. */ + it.skipIf(!process.env.KNOWLEDGE_SEARCH_ASSISTANT_URL)( + 'answers through local Go Assistant with progressive reads and citations', + async () => { + const assistantUrl = new URL(process.env.KNOWLEDGE_SEARCH_ASSISTANT_URL!) + const simUrl = new URL(process.env.KNOWLEDGE_SEARCH_SIM_URL!) + for (const url of [assistantUrl, simUrl]) { + if (!['127.0.0.1', 'localhost'].includes(url.hostname)) + throw new Error( + 'Assistant integration requires local servers using the disposable test databases' + ) + } + const apiKey = process.env.KNOWLEDGE_SEARCH_ASSISTANT_API_KEY! + const internalKey = process.env.KNOWLEDGE_SEARCH_SIM_INTERNAL_KEY! + expect(apiKey).toBeTruthy() + expect(internalKey).toBeTruthy() + const chunkId = `${ids.workspaceId}-chunk-0` + const [original] = await db + .select({ + content: embedding.content, + contentLength: embedding.contentLength, + tokenCount: embedding.tokenCount, + }) + .from(embedding) + .where(eq(embedding.id, chunkId)) + .limit(1) + const longContent = + 'Orion deployment guide. The activation phrase and final checksum appear at the end.\n' + + 'Review the deployment stages in order. Preserve the rollback procedure.\n'.repeat(320) + + '\nActivation phrase: SILVER COMET\nFinal checksum: K7M2-84\n' + const registry = new ResolvedSecretTraceRegistry([], { userId: ids.aliceId }) + const calls: Array<{ + name: string + arguments: unknown + milliseconds: number + bytes: number + }> = [] + let answer = '' + const started = performance.now() + try { + await db + .update(embedding) + .set({ + content: longContent, + contentLength: longContent.length, + tokenCount: Math.ceil(longContent.length / 4), + }) + .where(eq(embedding.id, chunkId)) + const admission = await externalFetch(new URL('/api/copilot/api-keys/validate', simUrl), { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': internalKey, + 'x-sim-billing-protocol': 'legacy-v0', + }, + body: JSON.stringify({ + userId: ids.aliceId, + organizationId: ids.organizationId, + chatId: organizationChatId, + }), + }) + expect(admission.status, await admission.text()).toBe(200) + let path = '/api/mothership' + let body: Record = { + message: + 'Search for the Orion deployment guide that mentions an activation phrase and final checksum. Read enough of that document to report both values and cite it.', + version: '3.0.0', + mode: 'assistant', + userId: ids.aliceId, + organizationId: ids.organizationId, + chatId: organizationChatId, + } + for (let round = 0; round < 8; round++) { + const response = await externalFetch(new URL(path, assistantUrl), { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': apiKey, + 'x-sim-billing-protocol': 'legacy-v0', + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(120000), + }) + const wire = await response.text() + expect(response.status, wire.slice(0, 2000)).toBe(200) + expect(Buffer.byteLength(wire)).toBeLessThan(2 * 1024 * 1024) + const pending = new Map() + let checkpoint: MothershipStreamV1CheckpointPausePayload | undefined + let streamId = '' + for (const line of wire.split('\n')) { + if (!line.startsWith('data:')) continue + const raw = line.slice(5).trim() + if (!raw || raw === '[DONE]') continue + const event: unknown = JSON.parse(raw) + if (!isContractStreamEventEnvelope(event)) + throw new Error('Assistant returned an invalid generated stream envelope') + streamId = event.stream.streamId + if (event.type === 'error') throw new Error(JSON.stringify(event.payload)) + if (event.type === 'text') answer += event.payload.text + if ( + event.type === 'tool' && + event.payload.phase === 'call' && + !event.payload.partial && + event.payload.arguments + ) + pending.set(event.payload.toolCallId, event.payload) + if (event.type === 'run' && event.payload.kind === 'checkpoint_pause') + checkpoint = event.payload + } + if (!checkpoint) break + const results = await Promise.all( + checkpoint.pendingToolCallIds.map(async (callId) => { + const call = pending.get(callId) + if (!call) throw new Error('Checkpoint referenced an absent tool call') + const tool = + call.toolName === 'search_workspace' + ? searchWorkspaceServerTool + : call.toolName === 'read_document' + ? readDocumentServerTool + : undefined + if (!tool) throw new Error(`Unexpected Assistant tool: ${call.toolName}`) + const toolStarted = performance.now() + const result = await tool.execute(call.arguments, { + userId: ids.aliceId, + organizationId: ids.organizationId, + chatId: organizationChatId, + toolCallId: callId, + copilotToolExecution: true, + requestMode: 'assistant', + resolvedSecretTraceRegistry: registry, + }) + calls.push({ + name: call.toolName, + arguments: call.arguments, + milliseconds: performance.now() - toolStarted, + bytes: Buffer.byteLength(JSON.stringify(result)), + }) + const { success } = z.object({ success: z.boolean() }).parse(result) + return { callId, name: call.toolName, success, data: result } + }) + ) + path = '/api/tools/resume' + body = { + checkpointId: checkpoint.checkpointId, + streamId, + userId: ids.aliceId, + organizationId: ids.organizationId, + chatId: organizationChatId, + results, + } + report['assistant.live.progress'] = { rounds: round + 1, calls } + saveReport() + } + report['assistant.live'] = { milliseconds: performance.now() - started, calls, answer } + saveReport() + expect(answer).toContain('SILVER COMET') + expect(answer).toContain('K7M2-84') + expect(answer).toContain('') + expect(calls.some((call) => call.name === 'read_document')).toBe(true) + expect(calls.some((call) => call.name === 'search_workspace')).toBe(true) + expect(calls.every((call) => call.bytes < 40000)).toBe(true) + } finally { + await db.update(embedding).set(original).where(eq(embedding.id, chunkId)) + } + }, + 10 * 60_000 + ) }) diff --git a/apps/sim/lib/knowledge/application/read-search-document.test.ts b/apps/sim/lib/knowledge/application/read-search-document.test.ts index ddd93ad5b0a..ad3b3831013 100644 --- a/apps/sim/lib/knowledge/application/read-search-document.test.ts +++ b/apps/sim/lib/knowledge/application/read-search-document.test.ts @@ -56,8 +56,7 @@ const context = { const input = { documentId: 'doc', assertedWorkspaceId: 'workspace', - offset: 0, - limit: 20, + limit: 3, filters: { source: 'slack', documentIds: ['doc'] }, resultSecretRegistry: new ResolvedSecretTraceRegistry([], { userId: 'reader', @@ -89,7 +88,7 @@ describe('Assistant document read', () => { await expect(readSearchDocument.execute({ principal, input })).resolves.toMatchObject({ documentId: 'doc', chunks: [{ content: 'body', chunkIndex: 0 }], - nextOffset: null, + next: null, }) expect(mocks.context).toHaveBeenCalledWith({ ...input, knowledgeBaseId: 'index' }, principal) expect(mocks.chunks).toHaveBeenCalledWith( @@ -97,8 +96,7 @@ describe('Assistant document read', () => { expect.objectContaining({ documentFilters: input.filters, enabled: 'true', - offset: 0, - limit: 20, + limit: 3, }), expect.any(String), access @@ -194,3 +192,92 @@ describe('organization Search document reads', () => { expect(mocks.chunks).not.toHaveBeenCalled() }) }) + +describe('precise bounded passage expansion', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.permission.mockResolvedValue('read') + mocks.context.mockResolvedValue(context) + mocks.provenance.mockResolvedValue({ imported: true, documentMetadata: {} }) + }) + + it('projects a secret spanning the page boundary before slicing it', async () => { + const secret = 'private-token-that-crosses-the-boundary' + const registry = new ResolvedSecretTraceRegistry([ + { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, + ]) + mocks.provenance.mockImplementationOnce(async () => { + registry.recordResolved('TOKEN', secret) + return { imported: true, documentMetadata: {} } + }) + mocks.chunks.mockResolvedValue({ + chunks: [{ id: 'secret-chunk', chunkIndex: 0, content: `${'x'.repeat(7990) + secret}tail` }], + pagination: { total: 1, hasMore: false }, + }) + const result = await readSearchDocument.execute({ + principal, + input: { ...input, resultSecretRegistry: registry }, + }) + expect(result.chunks[0].content).toContain('{{TOKEN}}') + expect(JSON.stringify(result)).not.toContain('private-token') + }) + + it('continues a long chunk before advancing across disabled chunk gaps', async () => { + const content = 'Evidence 🔎\n'.repeat(1100) + mocks.chunks.mockResolvedValue({ + chunks: [ + { id: 'c7', chunkIndex: 7, content }, + { id: 'c11', chunkIndex: 11, content: 'next enabled passage' }, + ], + pagination: { total: 2, hasMore: false }, + }) + const first = await readSearchDocument.execute({ + principal, + input: { ...input, startChunkIndex: 7 }, + }) + expect(first.chunks).toHaveLength(1) + expect(first.chunks[0].content.length).toBeLessThanOrEqual(8000) + expect(first.next).toEqual({ startChunkIndex: 7, startOffset: first.chunks[0].endOffset }) + const second = await readSearchDocument.execute({ + principal, + input: { ...input, ...first.next! }, + }) + expect(first.chunks[0].content + second.chunks[0].content).toBe(content) + expect(second.chunks[1].chunkIndex).toBe(11) + expect(second.next).toBeNull() + expect(mocks.chunks).toHaveBeenCalledWith( + 'doc', + expect.objectContaining({ startChunkIndex: 7, requireEnabledDocument: true }), + expect.any(String), + access + ) + }) + + it('rejects a continuation when all remaining chunks have disappeared', async () => { + mocks.chunks.mockResolvedValue({ + chunks: [], + pagination: { total: 2, hasMore: false }, + }) + await expect( + readSearchDocument.execute({ principal, input: { ...input, startChunkIndex: 7 } }) + ).rejects.toThrow('no longer available') + expect(mocks.provenance).not.toHaveBeenCalled() + }) + + it('rejects positions without an anchor and stale within-chunk continuation', async () => { + await expect( + readSearchDocument.execute({ principal, input: { ...input, startOffset: 2 } }) + ).rejects.toThrow('startOffset requires startChunkIndex') + expect(mocks.chunks).not.toHaveBeenCalled() + mocks.chunks.mockResolvedValue({ + chunks: [{ id: 'c8', chunkIndex: 8, content: 'replacement' }], + pagination: { total: 1, hasMore: false }, + }) + await expect( + readSearchDocument.execute({ + principal, + input: { ...input, startChunkIndex: 7, startOffset: 100 }, + }) + ).rejects.toThrow('no longer available') + }) +}) diff --git a/apps/sim/lib/knowledge/application/read-search-document.ts b/apps/sim/lib/knowledge/application/read-search-document.ts index ac48bcdbbe3..dbc52ccc0b5 100644 --- a/apps/sim/lib/knowledge/application/read-search-document.ts +++ b/apps/sim/lib/knowledge/application/read-search-document.ts @@ -8,9 +8,12 @@ import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chun import { resolveCanonicalActiveKnowledgeDocumentContext } from '@/lib/knowledge/application/contexts' import { knowledgeOperations } from '@/lib/knowledge/application/operations' import { queryChunks } from '@/lib/knowledge/chunks/service' +import { measureSearchStage } from '@/lib/knowledge/search/diagnostics' import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters' import { findSearchIndex } from '@/lib/knowledge/search/search-index' +import { passageWindow } from '@/lib/knowledge/search/snippet' import { importKnowledgeSearchResultSecretProvenance } from '@/lib/knowledge/secret-provenance' +import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' export interface ReadSearchDocumentInput { @@ -18,12 +21,19 @@ export interface ReadSearchDocumentInput { assertedWorkspaceId?: string assertedOrganizationId?: string filters?: WorkspaceSearchFilters - offset: number limit: number + startChunkIndex?: number + startOffset?: number resultSecretRegistry: ResolvedSecretTraceRegistry signal?: AbortSignal } +/** Bounds model text to at most 24KB of UTF-8, with continuation even inside a large chunk. */ +const READ_PAGE_CHARACTERS = 8000 +const READ_PAGE_CHUNKS = 8 +const STALE_POSITION_MESSAGE = + 'The passage position is no longer available; search again or read from the chunk start' + /** Reads enabled indexed passages with the same document scope and ACLs as search. */ export const readSearchDocument = defineAuthorizedKnowledgeUseCase({ operation: knowledgeOperations.readDocument, @@ -50,16 +60,22 @@ export const readSearchDocument = defineAuthorizedKnowledgeUseCase({ async execute({ input, context }): Promise { input.signal?.throwIfAborted() if ( - !Number.isInteger(input.offset) || - input.offset < 0 || - input.offset > 5000 || !Number.isInteger(input.limit) || input.limit < 1 || - input.limit > 50 + input.limit > READ_PAGE_CHUNKS || + (input.startChunkIndex !== undefined && + (!Number.isSafeInteger(input.startChunkIndex) || + input.startChunkIndex < 0 || + input.startChunkIndex > 2147483647)) || + (input.startOffset !== undefined && + (!Number.isSafeInteger(input.startOffset) || + input.startOffset < 0 || + input.startOffset > 2147483647 || + input.startChunkIndex === undefined)) ) { throw new OrchestrationError( 'validation', - 'Document reads require offset 0–5000 and limit 1–50' + 'Document reads require limit 1–8 and nonnegative chunk positions; startOffset requires startChunkIndex' ) } if (context.document.processingStatus !== 'completed') { @@ -68,24 +84,33 @@ export const readSearchDocument = defineAuthorizedKnowledgeUseCase({ if (!context.knowledgeBase.isSearchIndex) throw new OrchestrationError('not_found', 'Document not found') if (!context.document.enabled) throw new OrchestrationError('not_found', 'Document not found') - const page = await queryChunks( - context.documentId, - { - offset: input.offset, - limit: input.limit, - enabled: 'true', - sortBy: 'chunkIndex', - sortOrder: 'asc', - documentFilters: input.filters, - }, - generateRequestId(), - await context.access.get() + const access = await measureSearchStage('access_scope', () => context.access.get()) + const page = await measureSearchStage('document_read.sql', () => + queryChunks( + context.documentId, + { + limit: input.limit, + startChunkIndex: input.startChunkIndex, + requireEnabledDocument: true, + enabled: 'true', + sortBy: 'chunkIndex', + sortOrder: 'asc', + documentFilters: input.filters, + }, + generateRequestId(), + access + ) ) if (page.pagination.total === 0) throw new OrchestrationError('not_found', 'Document not found') - const provenance = await importKnowledgeSearchResultSecretProvenance({ - registry: input.resultSecretRegistry, - results: page.chunks.map((chunk) => ({ ...chunk, documentId: context.documentId })), - }) + if (input.startChunkIndex !== undefined && page.chunks.length === 0) { + throw new OrchestrationError('validation', STALE_POSITION_MESSAGE) + } + const provenance = await measureSearchStage('result_provenance', () => + importKnowledgeSearchResultSecretProvenance({ + registry: input.resultSecretRegistry, + results: page.chunks.map((chunk) => ({ ...chunk, documentId: context.documentId })), + }) + ) if (!provenance.imported) throw new Error('Knowledge result provenance is unavailable') const metadata = provenance.documentMetadata[context.documentId] if ( @@ -99,15 +124,50 @@ export const readSearchDocument = defineAuthorizedKnowledgeUseCase({ ) { throw new Error('Knowledge document provenance is unavailable') } + /** Project complete strings before slicing, so a window cannot expose part of a secret. */ + const projectedChunks = page.chunks.map((chunk) => { + const projected = projectResolvedSecretModelContent(chunk.content, input.resultSecretRegistry) + if (!projected.safe || typeof projected.value !== 'string') + throw new Error('Knowledge result provenance is unavailable') + return { ...chunk, content: projected.value } + }) + if ( + input.startOffset && + (projectedChunks[0]?.chunkIndex !== input.startChunkIndex || + input.startOffset >= projectedChunks[0].content.length) + ) { + throw new OrchestrationError('validation', STALE_POSITION_MESSAGE) + } + let remaining = READ_PAGE_CHARACTERS + const chunks: ReadSearchDocumentResult['chunks'] = [] + let next: ReadSearchDocumentResult['next'] = null + for (const chunk of projectedChunks) { + if (remaining < 2) { + next = { startChunkIndex: chunk.chunkIndex, startOffset: 0 } + break + } + const start = chunk.chunkIndex === input.startChunkIndex ? (input.startOffset ?? 0) : 0 + const excerpt = passageWindow(chunk.content, start, remaining) + chunks.push({ chunkIndex: chunk.chunkIndex, ...excerpt }) + remaining -= excerpt.content.length + if (excerpt.endOffset < chunk.content.length) { + next = { startChunkIndex: chunk.chunkIndex, startOffset: excerpt.endOffset } + break + } + } + const last = chunks.at(-1) + if (!next && page.pagination.hasMore && last) { + next = { startChunkIndex: last.chunkIndex + 1, startOffset: 0 } + } input.signal?.throwIfAborted() return { documentId: context.documentId, knowledgeBaseId: context.knowledgeBaseId, documentName: metadata?.filename ?? null, sourceUrl: metadata?.sourceUrl ?? null, - chunks: page.chunks.map(({ content, chunkIndex }) => ({ content, chunkIndex })), - hasMore: page.pagination.hasMore, - nextOffset: page.pagination.hasMore ? input.offset + page.chunks.length : null, + chunks, + hasMore: next !== null, + next, } }, }) diff --git a/apps/sim/lib/knowledge/application/search.test.ts b/apps/sim/lib/knowledge/application/search.test.ts index 49aa6d88584..9adf6797f47 100644 --- a/apps/sim/lib/knowledge/application/search.test.ts +++ b/apps/sim/lib/knowledge/application/search.test.ts @@ -89,7 +89,10 @@ vi.mock('@/lib/knowledge/embeddings', () => ({ vi.mock('@/lib/knowledge/search/queries', () => ({ generateSearchEmbedding: mocks.generateEmbedding, - executeKnowledgeSearch: mocks.executeSearch, + retrieveKnowledgeSearch: async (...args: unknown[]) => ({ + rows: await mocks.executeSearch(...args), + retrieval: { status: 'complete', timedOutLegs: [] }, + }), getDocumentMetadataByIds: mocks.getDocumentMetadata, })) diff --git a/apps/sim/lib/knowledge/application/search.ts b/apps/sim/lib/knowledge/application/search.ts index 1bcd42670b8..b67f2fef3fc 100644 --- a/apps/sim/lib/knowledge/application/search.ts +++ b/apps/sim/lib/knowledge/application/search.ts @@ -41,12 +41,14 @@ import { runWithKnowledgeModelInputProvenance } from '@/lib/knowledge/model-inpu import { rerank } from '@/lib/knowledge/reranker' import type { RerankerStatus } from '@/lib/knowledge/reranker-models' import { recordOrganizationSearchActivity } from '@/lib/knowledge/search/activity' +import { SearchDeadlineError } from '@/lib/knowledge/search/budget' import { resolveKnowledgeSearchDefaults } from '@/lib/knowledge/search/defaults' import { annotateSearchDiagnostics, measureSearchStage } from '@/lib/knowledge/search/diagnostics' import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters' import { - executeKnowledgeSearch, getDocumentMetadataByIds, + type RetrievalStatus, + retrieveKnowledgeSearch, type SearchResult, } from '@/lib/knowledge/search/queries' import { importKnowledgeSearchResultSecretProvenance } from '@/lib/knowledge/secret-provenance' @@ -88,6 +90,8 @@ export class KnowledgeSearchProvenanceUnavailableError extends Error { export type KnowledgeSearchTagFilter = KnowledgeTagNameFilter export interface SearchKnowledgeInput { + /** Only surfaces displaying retrieval status may accept incomplete evidence. */ + allowPartialResults?: boolean /** Optional assertion from a trusted adapter or public contract. */ workspaceId?: string organizationId?: string @@ -154,6 +158,7 @@ interface KnowledgeSearchCost { } export interface SearchKnowledgeResult { + retrieval: RetrievalStatus results: KnowledgeSearchItem[] query: string knowledgeBaseIds: string[] @@ -397,8 +402,8 @@ const searchKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ ) : Math.min(KNOWLEDGE_SEARCH_COST_POLICY.maxTopK, input.topK * 4) : input.topK - let rows = await measureSearchStage('retrieval', () => - executeKnowledgeSearch({ + const retrieved = await measureSearchStage('retrieval', () => + retrieveKnowledgeSearch({ knowledgeBaseIds, topK: candidateTopK, filters: input.filters, @@ -418,6 +423,13 @@ const searchKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ }) ) + if (retrieved.retrieval.status === 'partial' && !input.allowPartialResults) + throw new SearchDeadlineError() + annotateSearchDiagnostics({ + retrievalStatus: retrieved.retrieval.status, + timedOutLegs: retrieved.retrieval.timedOutLegs, + }) + let rows = retrieved.rows input.signal?.throwIfAborted() /** Public callers have no input envelope, but persisted reranker inputs still need provenance. */ const registrySubjectUserId = resolvePrincipalSubjectUserId(principal) @@ -697,6 +709,9 @@ const searchKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ } } annotateSearchDiagnostics({ resultCount: results.length }) + if (retrieved.retrieval.status === 'partial' && results.length === 0) { + throw new SearchDeadlineError() + } const cost = baseCost ? { input: baseCost.input, @@ -715,6 +730,7 @@ const searchKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ } : undefined return { + retrieval: retrieved.retrieval, results, query: input.query ?? '', knowledgeBaseIds, diff --git a/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts b/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts index 32ecacf0059..301766ff25f 100644 --- a/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts +++ b/apps/sim/lib/knowledge/application/workspace-search.activity.test.ts @@ -63,6 +63,7 @@ describe.each([ queueTableRows(member, [{ role: 'member' }]) expect(await operation.execute({ principal, input })).toEqual({ results: [], + retrieval: { status: 'complete', timedOutLegs: [] }, query: 'policy', knowledgeBases: [], }) diff --git a/apps/sim/lib/knowledge/application/workspace-search.ts b/apps/sim/lib/knowledge/application/workspace-search.ts index 983c7819011..81924891d5a 100644 --- a/apps/sim/lib/knowledge/application/workspace-search.ts +++ b/apps/sim/lib/knowledge/application/workspace-search.ts @@ -31,7 +31,13 @@ const searchWorkspaceKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ const index = await measureSearchStage('index_resolution', () => findWorkspaceSearchIndex(context.workspaceId) ) - if (!index) return { results: [], query: input.query ?? '', knowledgeBases: [] } + if (!index) + return { + results: [], + query: input.query ?? '', + knowledgeBases: [], + retrieval: { status: 'complete' as const, timedOutLegs: [] }, + } return searchKnowledge.execute({ principal, input: { ...input, workspaceId: context.workspaceId, knowledgeBaseIds: [index.id] }, @@ -75,7 +81,12 @@ const searchOrganizationKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ results: [], }) } - return { results: [], query: input.query ?? '', knowledgeBases: [] } + return { + results: [], + query: input.query ?? '', + knowledgeBases: [], + retrieval: { status: 'complete' as const, timedOutLegs: [] }, + } } return searchKnowledge.execute({ principal, @@ -118,7 +129,12 @@ const searchScopedKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ results: [], }) } - return { results: [], query: input.query ?? '', knowledgeBases: [] } + return { + results: [], + query: input.query ?? '', + knowledgeBases: [], + retrieval: { status: 'complete' as const, timedOutLegs: [] }, + } } return searchKnowledge.execute({ principal, diff --git a/apps/sim/lib/knowledge/chunks/service.ts b/apps/sim/lib/knowledge/chunks/service.ts index 02a018367c9..f471c084f81 100644 --- a/apps/sim/lib/knowledge/chunks/service.ts +++ b/apps/sim/lib/knowledge/chunks/service.ts @@ -3,7 +3,7 @@ import { document, embedding, knowledgeBase } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' -import { and, eq, inArray, isNull, sql } from 'drizzle-orm' +import { and, eq, gte, inArray, isNull, sql } from 'drizzle-orm' import { type KeysetKey, keysetColumns, @@ -123,7 +123,13 @@ export async function queryChunks( * keyset resume narrows the *page*, and folding it into the count would turn * a total into a remainder that shrinks with every page. */ - const pageConditions = [...conditions, resumeKeyset(keys, cursorKeys, sortOrder)] + const pageConditions = [ + ...conditions, + resumeKeyset(keys, cursorKeys, sortOrder), + filters.startChunkIndex === undefined + ? undefined + : gte(embedding.chunkIndex, filters.startChunkIndex), + ] const rows = await db .select({ diff --git a/apps/sim/lib/knowledge/chunks/types.ts b/apps/sim/lib/knowledge/chunks/types.ts index 66c20011a18..4225a2e28e6 100644 --- a/apps/sim/lib/knowledge/chunks/types.ts +++ b/apps/sim/lib/knowledge/chunks/types.ts @@ -13,6 +13,8 @@ export interface ChunkFilters { enabled?: 'true' | 'false' | 'all' limit?: number offset?: number + /** Inclusive indexed position; unlike an ordinal offset it survives disabled chunk gaps. */ + startChunkIndex?: number sortBy?: ChunkSortBy sortOrder?: 'asc' | 'desc' /** Keyset position from a previous page. Never combined with `offset`. */ diff --git a/apps/sim/lib/knowledge/search/budget.test.ts b/apps/sim/lib/knowledge/search/budget.test.ts new file mode 100644 index 00000000000..d5e04601737 --- /dev/null +++ b/apps/sim/lib/knowledge/search/budget.test.ts @@ -0,0 +1,55 @@ +/** @vitest-environment node */ +import { db } from '@sim/db' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { SearchBudget, SearchDeadlineError } from '@/lib/knowledge/search/budget' + +afterEach(() => vi.restoreAllMocks()) + +describe('search SQL deadline', () => { + it('does not start a fallback with a fresh budget', async () => { + let now = 0 + vi.spyOn(performance, 'now').mockImplementation(() => now) + const budget = new SearchBudget('vector', 100) + const run = vi.fn(async () => { + now = 101 + return ['candidate'] + }) + await budget.query('vector.ann', run) + await expect(budget.query('vector.exact', run)).rejects.toBeInstanceOf(SearchDeadlineError) + expect(run).toHaveBeenCalledTimes(1) + }) + + it('returns at the acquisition deadline and skips work when the queued transaction later starts', async () => { + vi.useFakeTimers() + try { + let begin: (() => void) | undefined + let finished: Promise | undefined + vi.spyOn(db, 'transaction').mockImplementation((callback) => { + finished = new Promise((resolve) => { + begin = resolve + }).then(() => callback(db as never)) + return finished as ReturnType + }) + const run = vi.fn(async () => ['private result']) + const budget = new SearchBudget('keyword', performance.now() + 50) + const pending = budget.query('keyword.sql', run) + const assertion = expect(pending).rejects.toBeInstanceOf(SearchDeadlineError) + await vi.advanceTimersByTimeAsync(60) + await assertion + begin!() + await expect(finished).rejects.toBeInstanceOf(SearchDeadlineError) + expect(run).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + + it('preserves cancellation and unexpected errors instead of labeling them incomplete evidence', async () => { + const controller = new AbortController() + const budget = new SearchBudget('keyword', performance.now() - 1, controller.signal) + const denied = new Error('access revoked') + expect(budget.isTimeout(denied)).toBe(false) + controller.abort(denied) + expect(() => budget.isTimeout(new SearchDeadlineError())).toThrow(denied) + }) +}) diff --git a/apps/sim/lib/knowledge/search/budget.ts b/apps/sim/lib/knowledge/search/budget.ts new file mode 100644 index 00000000000..7f8a0eaeecf --- /dev/null +++ b/apps/sim/lib/knowledge/search/budget.ts @@ -0,0 +1,115 @@ +import { db } from '@sim/db' +import { getPostgresErrorCode } from '@sim/utils/errors' +import { sql } from 'drizzle-orm' +import type { DbTransaction } from '@/lib/db/types' +import { + measureSearchStage, + recordSearchStageDuration, + type SearchStage, +} from '@/lib/knowledge/search/diagnostics' + +export const SEARCH_RETRIEVAL_BUDGET_MS = 8000 +export type RetrievalLeg = 'vector' | 'keyword' | 'tags' +export type SearchExecutor = Pick + +export class SearchDeadlineError extends Error { + constructor() { + super('Search reached its retrieval deadline. Retry with a narrower query or source filter.') + this.name = 'SearchDeadlineError' + } +} + +/** One leg's state, using the hybrid request's shared deadline across every refill and fallback. */ +export class SearchBudget { + timedOut = false + + constructor( + readonly leg: RetrievalLeg, + readonly deadline: number, + readonly signal?: AbortSignal + ) {} + + remaining(): number { + this.signal?.throwIfAborted() + const remaining = Math.ceil(this.deadline - performance.now()) + if (remaining <= 0) { + this.timedOut = true + throw new SearchDeadlineError() + } + return remaining + } + + isTimeout(error: unknown): boolean { + this.signal?.throwIfAborted() + if (error instanceof SearchDeadlineError || getPostgresErrorCode(error) === '57014') { + this.timedOut = true + return true + } + return false + } + + /** + * Only acquisition is raced. An expired queued transaction rolls back before executing work. + * Once acquired, PostgreSQL cancels the statement and we await rollback/release before returning. + * This avoids postgres.js cancellation packets racing a subsequent query on a pooled connection. + */ + async query( + stage: SearchStage, + run: (executor: SearchExecutor) => PromiseLike + ): Promise { + const started = performance.now() + const remaining = this.remaining() + let acquired = false + let expired = false + let timer: ReturnType | undefined + let rejectAcquisition: (error: unknown) => void = () => {} + const onAbort = () => { + if (!acquired) { + expired = true + rejectAcquisition(this.signal?.reason) + } + } + const acquisition = new Promise((_, reject) => { + rejectAcquisition = reject + timer = setTimeout(() => { + expired = true + this.timedOut = true + reject(new SearchDeadlineError()) + }, remaining) + this.signal?.addEventListener('abort', onAbort, { once: true }) + }) + const work = db.transaction(async (tx) => { + acquired = true + clearTimeout(timer) + this.signal?.removeEventListener('abort', onAbort) + if (expired) throw new SearchDeadlineError() + recordSearchStageDuration(`${this.leg}.connection_acquire`, performance.now() - started) + const timeout = String(this.remaining()) + await tx.execute(sql`SELECT set_config('statement_timeout', ${timeout}, true)`) + this.remaining() + return measureSearchStage(stage, () => run(tx)) + }) + try { + const result = await Promise.race([work, acquisition]) + this.signal?.throwIfAborted() + return result + } catch (error) { + if (this.isTimeout(error)) throw new SearchDeadlineError() + throw error + } finally { + clearTimeout(timer) + this.signal?.removeEventListener('abort', onAbort) + if (!acquired) + recordSearchStageDuration(`${this.leg}.connection_acquire`, performance.now() - started) + } + } +} + +/** Execute SQL with stage diagnostics and, for live search, the shared retrieval deadline. */ +export function runSearchQuery( + budget: SearchBudget | undefined, + stage: SearchStage, + run: (executor: SearchExecutor) => PromiseLike +): Promise { + return budget ? budget.query(stage, run) : measureSearchStage(stage, () => run(db)) +} diff --git a/apps/sim/lib/knowledge/search/diagnostics.ts b/apps/sim/lib/knowledge/search/diagnostics.ts index 09672e26a76..bc8e5c4e921 100644 --- a/apps/sim/lib/knowledge/search/diagnostics.ts +++ b/apps/sim/lib/knowledge/search/diagnostics.ts @@ -7,6 +7,8 @@ const PROGRESS_INTERVAL_MS = 5000 type RetrievalLeg = 'vector' | 'keyword' | 'tags' export type SearchStage = + | 'document_read' + | 'document_read.sql' | 'tool_input' | 'tool_application' | 'tool_presentation' @@ -40,7 +42,8 @@ export type SearchStage = | `${RetrievalLeg}.candidates` | `${RetrievalLeg}.authorization` | `${RetrievalLeg}.hydration` - | 'vector.connection_acquire' + | `${RetrievalLeg}.connection_acquire` + | `${RetrievalLeg}.sql` | 'vector.settings' | 'vector.probe' | 'vector.ann' @@ -48,6 +51,7 @@ export type SearchStage = /** Fixed, content-free fields. Never pass queries, filters, document identities, SQL, or errors. */ export interface SearchDiagnosticMetadata { + operation?: 'search_workspace' | 'read_document' surface?: 'dashboard' | 'mcp' | 'copilot' | 'workflow' | 'api' | 'slack' | 'other' toolCallId?: string executionId?: string @@ -68,6 +72,9 @@ export interface SearchDiagnosticMetadata { /** Tool output before the executor's final egress projection; counts only, never content. */ toolResultBytes?: number passageBytes?: number + originalPassageBytes?: number + retrievalStatus?: 'complete' | 'partial' + timedOutLegs?: RetrievalLeg[] maxPassageBytes?: number uniqueDocumentCount?: number } diff --git a/apps/sim/lib/knowledge/search/queries.test.ts b/apps/sim/lib/knowledge/search/queries.test.ts index e9beb8eccc7..a1b9728d06e 100644 --- a/apps/sim/lib/knowledge/search/queries.test.ts +++ b/apps/sim/lib/knowledge/search/queries.test.ts @@ -630,7 +630,7 @@ describe('live repository authorization follows ranked candidates', () => { expect(rows.map((row) => row.id)).toEqual(['selected']) expect(dbChainMockFns.offset.mock.calls).toEqual([[0], [20], [0], [20]]) expect(getForConnectors).toHaveBeenCalledTimes(2) - expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(3) + expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(2) }) it('keeps the nearest exact results when an earlier ANN page hydrated only a farther result', async () => { diff --git a/apps/sim/lib/knowledge/search/queries.ts b/apps/sim/lib/knowledge/search/queries.ts index 17becb53e74..91ff5d12957 100644 --- a/apps/sim/lib/knowledge/search/queries.ts +++ b/apps/sim/lib/knowledge/search/queries.ts @@ -9,6 +9,14 @@ import { } from '@/lib/knowledge/access/predicate' import type { KnowledgeAccessProvider, KnowledgeAccessScope } from '@/lib/knowledge/access/types' import type { KbEmbeddingDimensions } from '@/lib/knowledge/embedding-models' +import { + type RetrievalLeg, + runSearchQuery, + SEARCH_RETRIEVAL_BUDGET_MS, + SearchBudget, + SearchDeadlineError, + type SearchExecutor, +} from '@/lib/knowledge/search/budget' import { measureSearchStage, recordSearchStageDuration } from '@/lib/knowledge/search/diagnostics' import { workspaceSearchFilterConditions } from '@/lib/knowledge/search/filter-conditions' import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters' @@ -33,8 +41,6 @@ const HNSW_SETTINGS_UNSUPPORTED_RETRY_MS = 10 * 60 * 1000 let hnswSettingsUnsupportedUntil = 0 -type SearchExecutor = Pick - /** * Shared HNSW indexes can be selective on KB scope, access, or tags, even for * small workspace searches. Iterative scans keep looking within a bounded @@ -42,14 +48,17 @@ type SearchExecutor = Pick * older extensions retry without tuning until the compatibility cooldown expires. */ async function withVectorScanSettings( - run: (executor: SearchExecutor) => Promise + run: (executor: SearchExecutor) => Promise, + budget?: SearchBudget ): Promise { - if (Date.now() < hnswSettingsUnsupportedUntil) return run(db) + const untuned = () => runSearchQuery(budget, 'vector.ann', run) + if (Date.now() < hnswSettingsUnsupportedUntil) return untuned() const acquireStarted = performance.now() let applyingSettings = false try { - return await db.transaction(async (tx) => { - recordSearchStageDuration('vector.connection_acquire', performance.now() - acquireStarted) + const tuned = async (tx: SearchExecutor) => { + if (!budget) + recordSearchStageDuration('vector.connection_acquire', performance.now() - acquireStarted) applyingSettings = true await measureSearchStage('vector.settings', () => tx.execute( @@ -57,15 +66,20 @@ async function withVectorScanSettings( ) ) applyingSettings = false + if (budget) + await tx.execute( + sql`SELECT set_config('statement_timeout', ${String(budget.remaining())}, true)` + ) return run(tx) - }) + } + return await (budget ? budget.query('vector.ann', tuned) : db.transaction(tuned)) } catch (error) { if (!applyingSettings || getPostgresErrorCode(error) !== UNDEFINED_OBJECT_SQLSTATE) throw error hnswSettingsUnsupportedUntil = Date.now() + HNSW_SETTINGS_UNSUPPORTED_RETRY_MS logger.warn('pgvector iterative scan is unavailable; vector legs run without it', { error: getErrorMessage(error), }) - return run(db) + return untuned() } } @@ -186,6 +200,7 @@ export interface SearchParams { access: KnowledgeAccessScope accessProvider?: KnowledgeAccessProvider signal?: AbortSignal + budget?: SearchBudget structuredFilters?: StructuredFilter[] filters?: WorkspaceSearchFilters queryVector?: KnowledgeQueryVector @@ -475,6 +490,7 @@ async function selectAuthorizedSearchResults(input: { accessProvider: KnowledgeAccessProvider filters?: WorkspaceSearchFilters signal?: AbortSignal + budget?: SearchBudget topK: number selectPage: ( limit: number, @@ -491,77 +507,82 @@ async function selectAuthorizedSearchResults(input: { const considered = new Set() let scanned = 0 let offset = 0 - while ( - results.size < input.topK && - scanned < Number(HNSW_MAX_SCAN_TUPLES) && - Date.now() < deadline - ) { - input.signal?.throwIfAborted() - const page = await measureSearchStage(`${input.leg}.candidates`, () => - input.selectPage(pageSize, offset, [...excludedSources]) - ) - if (!page.candidates.length) break - scanned += page.candidates.length - offset = page.nextOffset - const candidates = page.candidates.filter((candidate) => !considered.has(candidate.id)) - for (const candidate of candidates) considered.add(candidate.id) - if (!candidates.length) { - if (page.candidates.length < pageSize) break - continue - } - /** Candidate and hydration queries enforce this source filter; connector types are immutable. */ - const connectorIds = - input.filters?.source && - input.filters.source !== 'github' && - input.filters.source !== 'confluence' - ? [] - : [ - ...new Set( - candidates.flatMap((candidate) => - candidate.connectorId ? [candidate.connectorId] : [] - ) - ), - ] - const access = await measureSearchStage(`${input.leg}.authorization`, () => - input.accessProvider.getForConnectors(connectorIds, input.signal) - ) - input.signal?.throwIfAborted() - const grantedSources = new Set( - access.kind === 'user' - ? [ - ...(access.githubInstallationGrants?.map((grant) => grant.connectorId) ?? []), - ...(access.confluenceSiteGrants?.map((grant) => grant.connectorId) ?? []), - ] - : [] - ) - const excludedBefore = excludedSources.size - for (const candidate of candidates) { - if ( - candidate.liveAuthorizationSource && - candidate.connectorId && - !grantedSources.has(candidate.connectorId) + try { + while ( + results.size < input.topK && + scanned < Number(HNSW_MAX_SCAN_TUPLES) && + (input.budget !== undefined || Date.now() < deadline) + ) { + input.signal?.throwIfAborted() + input.budget?.remaining() + const page = await measureSearchStage(`${input.leg}.candidates`, () => + input.selectPage(pageSize, offset, [...excludedSources]) ) - excludedSources.add(candidate.connectorId) - } - const hydrated = await measureSearchStage(`${input.leg}.hydration`, () => - input.hydrate( - candidates.map((candidate) => candidate.id), - access + if (!page.candidates.length) break + scanned += page.candidates.length + offset = page.nextOffset + const candidates = page.candidates.filter((candidate) => !considered.has(candidate.id)) + for (const candidate of candidates) considered.add(candidate.id) + if (!candidates.length) { + if (page.candidates.length < pageSize) break + continue + } + /** Candidate and hydration queries enforce this source filter; connector types are immutable. */ + const connectorIds = + input.filters?.source && + input.filters.source !== 'github' && + input.filters.source !== 'confluence' + ? [] + : [ + ...new Set( + candidates.flatMap((candidate) => + candidate.connectorId ? [candidate.connectorId] : [] + ) + ), + ] + const access = await measureSearchStage(`${input.leg}.authorization`, () => + input.accessProvider.getForConnectors(connectorIds, input.signal) ) - ) - const byId = new Map(hydrated.map((row) => [row.id, row])) - for (const candidate of candidates) { - const row = byId.get(candidate.id) - if (row) results.set(row.id, row) - if (!input.compareResults && results.size === input.topK) break - } - if (input.compareResults) { - const ranked = [...results.values()].sort(input.compareResults).slice(0, input.topK) - results.clear() - for (const row of ranked) results.set(row.id, row) + input.signal?.throwIfAborted() + const grantedSources = new Set( + access.kind === 'user' + ? [ + ...(access.githubInstallationGrants?.map((grant) => grant.connectorId) ?? []), + ...(access.confluenceSiteGrants?.map((grant) => grant.connectorId) ?? []), + ] + : [] + ) + const excludedBefore = excludedSources.size + for (const candidate of candidates) { + if ( + candidate.liveAuthorizationSource && + candidate.connectorId && + !grantedSources.has(candidate.connectorId) + ) + excludedSources.add(candidate.connectorId) + } + const hydrated = await measureSearchStage(`${input.leg}.hydration`, () => + input.hydrate( + candidates.map((candidate) => candidate.id), + access + ) + ) + const byId = new Map(hydrated.map((row) => [row.id, row])) + for (const candidate of candidates) { + const row = byId.get(candidate.id) + if (row) results.set(row.id, row) + if (!input.compareResults && results.size === input.topK) break + } + if (input.compareResults) { + const ranked = [...results.values()].sort(input.compareResults).slice(0, input.topK) + results.clear() + for (const row of ranked) results.set(row.id, row) + } + if (excludedSources.size > excludedBefore) offset = 0 + else if (page.candidates.length < pageSize) break } - if (excludedSources.size > excludedBefore) offset = 0 - else if (page.candidates.length < pageSize) break + } catch (error) { + if (!input.budget?.isTimeout(error)) throw error } input.signal?.throwIfAborted() return [...results.values()] @@ -578,15 +599,19 @@ function hydrateSearchCandidates( access: KnowledgeAccessScope, distance: SQL | SQL.Aliased, filters: WorkspaceSearchFilters | undefined, - conditions: (SQL | undefined)[] + conditions: (SQL | undefined)[], + leg: RetrievalLeg, + budget?: SearchBudget ) { - return db - .select(getSearchResultFields(distance)) - .from(embedding) - .innerJoin(document, eq(embedding.documentId, document.id)) - .where( - and(inArray(embedding.id, ids), ...getVisibilityConditions(access, filters), ...conditions) - ) + return runSearchQuery(budget, `${leg}.sql`, (executor) => + executor + .select(getSearchResultFields(distance)) + .from(embedding) + .innerJoin(document, eq(embedding.documentId, document.id)) + .where( + and(inArray(embedding.id, ids), ...getVisibilityConditions(access, filters), ...conditions) + ) + ) } /** Candidates each hybrid leg retrieves before the fused list is trimmed to `topK`. */ @@ -629,26 +654,29 @@ export async function handleTagOnlySearch(params: SearchParams): Promise { - const candidates = await db - .select(SEARCH_READ_CANDIDATE_FIELDS) - .from(embedding) - .innerJoin(document, eq(embedding.documentId, document.id)) - .where( - and( - ...conditions, - ...getVisibilityConditions( - access, - params.filters, - knowledgeMetadataCandidateAccessCondition(access) - ), - excludeSearchSources(excludedSources) + const candidates = await runSearchQuery(params.budget, 'tags.sql', (executor) => + executor + .select(SEARCH_READ_CANDIDATE_FIELDS) + .from(embedding) + .innerJoin(document, eq(embedding.documentId, document.id)) + .where( + and( + ...conditions, + ...getVisibilityConditions( + access, + params.filters, + knowledgeMetadataCandidateAccessCondition(access) + ), + excludeSearchSources(excludedSources) + ) ) - ) - .orderBy(embedding.id) - .limit(limit) - .offset(offset) + .orderBy(embedding.id) + .limit(limit) + .offset(offset) + ) return { candidates, nextOffset: offset + candidates.length } }, hydrate: (ids, authorized) => @@ -657,7 +685,9 @@ export async function handleTagOnlySearch(params: SearchParams): Promise`0`.as('distance'), params.filters, - conditions + conditions, + 'tags', + params.budget ), }) } @@ -769,57 +799,58 @@ async function selectLiveVectorResults( accessProvider, filters: params.filters, signal: params.signal, + budget: params.budget, topK: params.topK, compareResults: (a, b) => a.distance - b.distance, - selectPage: (limit, offset, excludedSources) => - withVectorScanSettings(async (executor) => { - const visibility = [ - ...getVisibilityConditions( - params.access, - params.filters, - knowledgeMetadataCandidateAccessCondition(params.access) - ), - excludeSearchSources(excludedSources), - ] - /** Adding zero prevents an underfilled HNSW scan from being chosen again for fallback. */ - const exactPage = async (candidateIds?: string[]) => { - const exactOffset = useExactRanking ? offset : 0 - useExactRanking = true - const candidates = await measureSearchStage('vector.exact', () => - executor - .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: distance.as('distance') }) - .from(embedding) - .innerJoin(document, eq(embedding.documentId, document.id)) - .where( - and( - ...conditions, - ...visibility, - candidateIds ? inArray(embedding.id, candidateIds) : undefined - ) - ) - .orderBy(sql`(${distance}) + 0`, embedding.id) - .limit(limit) - .offset(exactOffset) - ) - return { candidates, nextOffset: exactOffset + candidates.length } - } - if (params.filters?.documentIds?.length || params.structuredFilters?.length) { - return exactPage() - } - /** Probe visibility without vector reads; revoked scopes must not detoast the corpus. */ - const probe = await measureSearchStage('vector.probe', () => + selectPage: async (limit, offset, excludedSources) => { + const visibility = [ + ...getVisibilityConditions( + params.access, + params.filters, + knowledgeMetadataCandidateAccessCondition(params.access) + ), + excludeSearchSources(excludedSources), + ] + /** Adding zero prevents an underfilled HNSW scan from being chosen again for fallback. */ + const exactPage = async (candidateIds?: string[]) => { + const exactOffset = useExactRanking ? offset : 0 + useExactRanking = true + const candidates = await runSearchQuery(params.budget, 'vector.exact', (executor) => executor - .select({ id: embedding.id }) + .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: distance.as('distance') }) .from(embedding) .innerJoin(document, eq(embedding.documentId, document.id)) - .where(and(inArray(embedding.knowledgeBaseId, params.knowledgeBaseIds), ...visibility)) - .limit(LIVE_SEARCH_PAGE_SIZE) + .where( + and( + ...conditions, + ...visibility, + candidateIds ? inArray(embedding.id, candidateIds) : undefined + ) + ) + .orderBy(sql`(${distance}) + 0`, embedding.id) + .limit(limit) + .offset(exactOffset) ) - if (probe.length === 0) return { candidates: [], nextOffset: offset } - if (probe.length < LIVE_SEARCH_PAGE_SIZE) { - return exactPage(probe.map((candidate) => candidate.id)) - } - if (useExactRanking) return exactPage() + return { candidates, nextOffset: exactOffset + candidates.length } + } + if (params.filters?.documentIds?.length || params.structuredFilters?.length) { + return exactPage() + } + /** Probe visibility without vector reads; revoked scopes must not detoast the corpus. */ + const probe = await runSearchQuery(params.budget, 'vector.probe', (executor) => + executor + .select({ id: embedding.id }) + .from(embedding) + .innerJoin(document, eq(embedding.documentId, document.id)) + .where(and(inArray(embedding.knowledgeBaseId, params.knowledgeBaseIds), ...visibility)) + .limit(LIVE_SEARCH_PAGE_SIZE) + ) + if (probe.length === 0) return { candidates: [], nextOffset: offset } + if (probe.length < LIVE_SEARCH_PAGE_SIZE) { + return exactPage(probe.map((candidate) => candidate.id)) + } + if (useExactRanking) return exactPage() + const ann = async (executor: SearchExecutor) => { const ranked = executor .select({ id: embedding.id, distance: distance.as('distance') }) .from(embedding) @@ -837,22 +868,30 @@ async function selectLiveVectorResults( .limit(limit) .offset(offset) .as('ranked_search_candidates') - const page = await measureSearchStage('vector.ann', () => - executor - .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: ranked.distance }) - .from(ranked) - .innerJoin(embedding, eq(embedding.id, ranked.id)) - .innerJoin(document, eq(document.id, embedding.documentId)) - .orderBy(ranked.distance, ranked.id) - ) - if (page.length < limit) return exactPage() - return { - candidates: page.sort((a, b) => a.distance - b.distance), - nextOffset: offset + page.length, - } - }), + return executor + .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: ranked.distance }) + .from(ranked) + .innerJoin(embedding, eq(embedding.id, ranked.id)) + .innerJoin(document, eq(document.id, embedding.documentId)) + .orderBy(ranked.distance, ranked.id) + } + const page = await withVectorScanSettings(ann, params.budget) + if (page.length < limit) return exactPage() + return { + candidates: page.sort((a, b) => a.distance - b.distance), + nextOffset: offset + page.length, + } + }, hydrate: (ids, authorized) => - hydrateSearchCandidates(ids, authorized, distance.as('distance'), params.filters, conditions), + hydrateSearchCandidates( + ids, + authorized, + distance.as('distance'), + params.filters, + conditions, + 'vector', + params.budget + ), }) /** Relaxed HNSW scans can return adjacent pages out of distance order. */ return rows.sort((a, b) => a.distance - b.distance) @@ -893,6 +932,7 @@ export interface KeywordSearchParams { access: KnowledgeAccessScope accessProvider?: KnowledgeAccessProvider signal?: AbortSignal + budget?: SearchBudget query: string /** Query embedding, so keyword-only hits still carry a real cosine distance. */ queryVector: KnowledgeQueryVector @@ -946,26 +986,29 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise accessProvider: params.accessProvider, filters: params.filters, signal: params.signal, + budget: params.budget, topK, selectPage: async (limit, offset, excludedSources) => { - const candidates = await db - .select({ ...SEARCH_READ_CANDIDATE_FIELDS, keywordRank: rankExpr.as('keyword_rank') }) - .from(embedding) - .innerJoin(document, eq(embedding.documentId, document.id)) - .where( - and( - ...conditions, - ...getVisibilityConditions( - access, - params.filters, - knowledgeMetadataCandidateAccessCondition(access) - ), - excludeSearchSources(excludedSources) + const candidates = await runSearchQuery(params.budget, 'keyword.sql', (executor) => + executor + .select({ ...SEARCH_READ_CANDIDATE_FIELDS, keywordRank: rankExpr.as('keyword_rank') }) + .from(embedding) + .innerJoin(document, eq(embedding.documentId, document.id)) + .where( + and( + ...conditions, + ...getVisibilityConditions( + access, + params.filters, + knowledgeMetadataCandidateAccessCondition(access) + ), + excludeSearchSources(excludedSources) + ) ) - ) - .orderBy(sql`${rankExpr} DESC`, embedding.id) - .limit(limit) - .offset(offset) + .orderBy(sql`${rankExpr} DESC`, embedding.id) + .limit(limit) + .offset(offset) + ) return { candidates, nextOffset: offset + candidates.length } }, hydrate: (ids, authorized) => @@ -974,7 +1017,9 @@ export async function executeKeywordSearch(params: KeywordSearchParams): Promise authorized, embeddingDistance(queryVector.dimensions, queryVector.vector).as('distance'), params.filters, - conditions + conditions, + 'keyword', + params.budget ), }) } @@ -1171,14 +1216,29 @@ export interface ExecuteKnowledgeSearchParams { filters?: WorkspaceSearchFilters } -/** - * Single retrieval entry point shared by the internal and v1 search routes. - * Callers remain responsible for auth, embedding generation, billing, and for - * rejecting requests that carry neither a query nor tag filters. - */ +export interface RetrievalStatus { + status: 'complete' | 'partial' + timedOutLegs: Array<'vector' | 'keyword' | 'tags'> +} + +export interface KnowledgeRetrievalResult { + rows: SearchResult[] + retrieval: RetrievalStatus +} + +/** Legacy surfaces cannot silently present partial retrieval as complete. */ export async function executeKnowledgeSearch( params: ExecuteKnowledgeSearchParams ): Promise { + const result = await retrieveKnowledgeSearch(params) + if (result.retrieval.status === 'partial') throw new SearchDeadlineError() + return result.rows +} + +/** Shared hybrid retrieval, with explicit completeness for surfaces that can represent it. */ +export async function retrieveKnowledgeSearch( + params: ExecuteKnowledgeSearchParams +): Promise { const { knowledgeBaseIds, topK, @@ -1189,99 +1249,69 @@ export async function executeKnowledgeSearch( access, boostRecency = false, } = params - + params.signal?.throwIfAborted() + const deadline = performance.now() + SEARCH_RETRIEVAL_BUDGET_MS + const budgets = { + vector: new SearchBudget('vector', deadline, params.signal), + keyword: new SearchBudget('keyword', deadline, params.signal), + tags: new SearchBudget('tags', deadline, params.signal), + } + const finish = (rows: SearchResult[]): KnowledgeRetrievalResult => { + params.signal?.throwIfAborted() + const timedOutLegs = Object.values(budgets) + .filter((budget) => budget.timedOut) + .map((budget) => budget.leg) + if (timedOutLegs.length && !rows.length) throw new SearchDeadlineError() + return { + rows: boostRecency ? applyRecencyBoost(rows) : rows, + retrieval: { status: timedOutLegs.length ? 'partial' : 'complete', timedOutLegs }, + } + } + const common = { + knowledgeBaseIds, + access, + accessProvider: params.accessProvider, + signal: params.signal, + filters: params.filters, + structuredFilters, + } const hasQuery = Boolean(query?.trim()) - const hasFilters = Boolean(structuredFilters && structuredFilters.length > 0) - + const hasFilters = Boolean(structuredFilters?.length) if (!hasQuery) { - if (!hasFilters) { - throw new Error('A search query or tag filters are required') - } - return await measureSearchStage('tags', () => - handleTagOnlySearch({ - knowledgeBaseIds, - topK, - structuredFilters, - access, - accessProvider: params.accessProvider, - signal: params.signal, - filters: params.filters, - }) + if (!hasFilters) throw new Error('A search query or tag filters are required') + return finish( + await measureSearchStage('tags', () => + handleTagOnlySearch({ ...common, topK, budget: budgets.tags }) + ) ) } - - if (!queryVector) { - throw new Error('Query vector is required when searching with a query') - } - + if (!queryVector) throw new Error('Query vector is required when searching with a query') const { distanceThreshold } = getQueryStrategy(knowledgeBaseIds.length, topK) - /** - * Hybrid fuses two rankings, so each leg retrieves more than the caller - * asked for: a chunk that both legs rank just below `topK` is a strong - * signal the fused list must be able to surface. - */ const legTopK = searchMode === 'hybrid' ? hybridCandidateCount(topK) : topK - + const vectorParams = { + ...common, + topK: legTopK, + queryVector, + distanceThreshold, + budget: budgets.vector, + } const vectorSearch = measureSearchStage('vector', () => - hasFilters - ? handleTagAndVectorSearch({ - knowledgeBaseIds, - topK: legTopK, - structuredFilters, - queryVector, - distanceThreshold, - access, - accessProvider: params.accessProvider, - signal: params.signal, - filters: params.filters, - }) - : handleVectorOnlySearch({ - knowledgeBaseIds, - topK: legTopK, - queryVector, - distanceThreshold, - access, - accessProvider: params.accessProvider, - signal: params.signal, - filters: params.filters, - }) + hasFilters ? handleTagAndVectorSearch(vectorParams) : handleVectorOnlySearch(vectorParams) ) - if (searchMode === 'vector') { - const results = await vectorSearch - return boostRecency ? applyRecencyBoost(results) : results - } - - /** - * The lexical leg is best-effort: a failure there falls back to vector-only - * results rather than failing the whole search. - */ + if (searchMode === 'vector') return finish(await vectorSearch) const keywordSearch = measureSearchStage('keyword', () => executeKeywordSearch({ - knowledgeBaseIds, + ...common, topK: legTopK, query: query!, queryVector, - structuredFilters, - access, - accessProvider: params.accessProvider, - signal: params.signal, - filters: params.filters, - }) - ).catch((error) => { - logger.warn('Keyword search leg failed; falling back to vector-only results', { - error: getErrorMessage(error, 'Unknown error'), + budget: budgets.keyword, }) - return [] as SearchResult[] - }) - - const [vectorResults, keywordResults] = await Promise.all([vectorSearch, keywordSearch]) - - /** - * Lexical leg first: on a total tie it wins, which is the behavior this mode - * exists for — an exact-token chunk the vector leg ranked below its distance - * threshold is precisely what a caller opted into hybrid to recover, and at - * `topK: 1` something has to win. - */ - const fused = fuseByReciprocalRank([keywordResults, vectorResults], topK) - return boostRecency ? applyRecencyBoost(fused) : fused + ) + const legs = await Promise.allSettled([vectorSearch, keywordSearch]) + /** Wait for both legs to release SQL resources; only deadline failures permit partial success. */ + for (const leg of legs) if (leg.status === 'rejected') throw leg.reason + const vectorResults = legs[0].status === 'fulfilled' ? legs[0].value : [] + const keywordResults = legs[1].status === 'fulfilled' ? legs[1].value : [] + return finish(fuseByReciprocalRank([keywordResults, vectorResults], topK)) } diff --git a/apps/sim/lib/knowledge/search/snippet.test.ts b/apps/sim/lib/knowledge/search/snippet.test.ts index 7dbec47ab5a..684e444d4d4 100644 --- a/apps/sim/lib/knowledge/search/snippet.test.ts +++ b/apps/sim/lib/knowledge/search/snippet.test.ts @@ -4,7 +4,9 @@ import { describe, expect, it } from 'vitest' import { findTermMatches, + matchPassage, matchSnippet, + passageWindow, queryTerms, SNIPPET_LENGTH, stripLeadingHeaders, @@ -138,3 +140,30 @@ describe('matchSnippet', () => { expect(snippet.endsWith('…')).toBe(true) }) }) + +describe('verbatim model passages', () => { + it('preserves formatting and exposes the location of evidence past the opening', () => { + const content = `Subject: Release\n\n${'Unrelated text. '.repeat(150)}\n\tdeploy_token = "orion"\n${'Trailing text. '.repeat(100)}` + const preview = matchPassage(content, 'orion', 1200) + expect(preview.content).toContain('\n\tdeploy_token = "orion"\n') + expect(preview.content).toBe(content.slice(preview.startOffset, preview.endOffset)) + expect(preview.content.length).toBeLessThanOrEqual(1200) + expect(preview.startOffset).toBeGreaterThan(0) + expect(preview.totalCharacters).toBe(content.length) + }) + + it('reassembles long Unicode chunks without splitting characters or losing text', () => { + const content = '中é🔎\n code();\n'.repeat(1900) + let position = 0 + let rebuilt = '' + while (position < content.length) { + const page = passageWindow(content, position, 8000) + expect(page.content.isWellFormed()).toBe(true) + expect(Buffer.byteLength(page.content)).toBeLessThanOrEqual(24000) + expect(page.endOffset).toBeGreaterThan(position) + rebuilt += page.content + position = page.endOffset + } + expect(rebuilt).toBe(content) + }) +}) diff --git a/apps/sim/lib/knowledge/search/snippet.ts b/apps/sim/lib/knowledge/search/snippet.ts index 648e7c4060a..dbc866c77fb 100644 --- a/apps/sim/lib/knowledge/search/snippet.ts +++ b/apps/sim/lib/knowledge/search/snippet.ts @@ -97,6 +97,45 @@ function alignToCodePoint(text: string, index: number): number { return unit >= 0xdc00 && unit <= 0xdfff ? index - 1 : index } +export interface PassageExcerpt { + content: string + /** Positions are UTF-16 code units in the input chunk. */ + startOffset: number + endOffset: number + totalCharacters: number +} + +/** A bounded, verbatim window; offsets make omitted text explicitly recoverable. */ +export function passageWindow( + content: string, + startOffset: number, + maxCharacters: number +): PassageExcerpt { + const start = alignToCodePoint(content, Math.min(startOffset, content.length)) + const end = alignToCodePoint(content, Math.min(start + maxCharacters, content.length)) + return { + content: content.slice(start, end), + startOffset: start, + endOffset: end, + totalCharacters: content.length, + } +} + +/** Search evidence preserves source formatting and anchors on the same matches as Search's UI. */ +export function matchPassage( + content: string, + query: string, + maxCharacters: number +): PassageExcerpt { + if (content.length <= maxCharacters) return passageWindow(content, 0, maxCharacters) + const anchor = findTermMatches(content, queryTerms(query)).reduce( + (best, match) => (!best || match.length > best.length ? match : best), + undefined + ) + const start = anchor ? Math.max(0, anchor.index - Math.floor(maxCharacters / 3)) : 0 + return passageWindow(content, start, maxCharacters) +} + /** * The passage of a document a search result shows: a window around the longest * matching query term, keeping the earliest occurrence on ties. This favors a From 29c604df0b66b2b7b97746eee23b784de568fa18 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 11 Sep 2026 16:25:31 -0700 Subject: [PATCH 03/15] fix(ci): bake the CLI telemetry token into the build that bun publish ships (#7790) --- .github/workflows/publish-sim-cli.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-sim-cli.yml b/.github/workflows/publish-sim-cli.yml index 53418f763d4..852423f1231 100644 --- a/.github/workflows/publish-sim-cli.yml +++ b/.github/workflows/publish-sim-cli.yml @@ -15,6 +15,13 @@ concurrency: jobs: publish-npm: + # Job-level, not on the build step: `bun publish` runs `prepublishOnly`, + # which rebuilds `dist` a second time, and that second build is the one + # that ships. A build without the token reports nothing. See + # docs/cli/usage-data. + env: + SIM_CLI_TELEMETRY_KEY: ${{ vars.SIM_CLI_TELEMETRY_KEY }} + SIM_CLI_TELEMETRY_HOST: ${{ vars.SIM_CLI_TELEMETRY_HOST }} runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 15 steps: @@ -63,11 +70,6 @@ jobs: - name: Build package working-directory: packages/sim-cli - env: - # Public PostHog project token for anonymous CLI usage reporting; a - # build without it reports nothing. See docs/cli/usage-data. - SIM_CLI_TELEMETRY_KEY: ${{ vars.SIM_CLI_TELEMETRY_KEY }} - SIM_CLI_TELEMETRY_HOST: ${{ vars.SIM_CLI_TELEMETRY_HOST }} run: bun run build - name: Resolve release channel From 9d7557af08c8a35e9199f5960514f6ee8552efc0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 11 Sep 2026 16:56:28 -0700 Subject: [PATCH 04/15] fix(rss): deliver unseen items published before the last poll (#7792) --- apps/sim/lib/webhooks/polling/rss.test.ts | 128 ++++++++++++++++++++++ apps/sim/lib/webhooks/polling/rss.ts | 14 ++- 2 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 apps/sim/lib/webhooks/polling/rss.test.ts diff --git a/apps/sim/lib/webhooks/polling/rss.test.ts b/apps/sim/lib/webhooks/polling/rss.test.ts new file mode 100644 index 00000000000..490e4a06bab --- /dev/null +++ b/apps/sim/lib/webhooks/polling/rss.test.ts @@ -0,0 +1,128 @@ +/** + * @vitest-environment node + */ +import { createLogger } from '@sim/logger' +import { createWorkflowRecord } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockFetch, mockValidateUrl, mockProcessEvent, mockUpdateConfig } = vi.hoisted(() => ({ + mockFetch: vi.fn(), + mockValidateUrl: vi.fn(), + mockProcessEvent: vi.fn(), + mockUpdateConfig: vi.fn(), +})) + +vi.mock('@/lib/core/security/input-validation.server', () => ({ + secureFetchWithPinnedIP: mockFetch, + validateUrlWithDNS: mockValidateUrl, +})) + +vi.mock('@/lib/core/idempotency/service', () => ({ + pollingIdempotency: { + executeWithIdempotency: vi.fn( + async (_provider: string, _key: string, execute: () => Promise) => execute() + ), + }, +})) + +vi.mock('@/lib/webhooks/processor', () => ({ + processPolledWebhookEvent: mockProcessEvent, +})) + +vi.mock('@/lib/webhooks/polling/utils', () => ({ + markWebhookSuccess: vi.fn(), + markWebhookFailed: vi.fn(), + updateWebhookProviderConfig: mockUpdateConfig, +})) + +import { rssPollingHandler } from '@/lib/webhooks/polling/rss' +import type { PollWebhookContext, WebhookRecord } from '@/lib/webhooks/polling/types' + +const SUBSCRIBED_AT = new Date('2026-08-27T18:36:16.000Z') +const LAST_CHECKED_AT = '2026-09-11T23:26:27.000Z' +const GUID = 'https://example.com/news/late-item' + +function context(lastSeenGuids: string[] = []): PollWebhookContext { + const webhookData: WebhookRecord = { + id: 'rss-webhook', + workflowId: 'rss-listener', + deploymentVersionId: null, + registrationStatus: null, + registrationGeneration: null, + configFingerprint: null, + preparedAt: null, + blockId: null, + path: 'rss-listener', + routingKey: null, + provider: 'rss', + providerConfig: { + feedUrl: 'https://example.com/feed.xml', + lastCheckedTimestamp: LAST_CHECKED_AT, + lastSeenGuids, + }, + isActive: true, + failedCount: 0, + lastFailedAt: null, + archivedAt: null, + createdAt: SUBSCRIBED_AT, + updatedAt: new Date(LAST_CHECKED_AT), + } + return { + webhookData, + workflowData: createWorkflowRecord({ + id: 'rss-listener', + }) as PollWebhookContext['workflowData'], + requestId: 'rss-request', + logger: createLogger('RssTest'), + } +} + +function feed(pubDate: string) { + return new Response( + ` + Canary fixturehttps://example.comRSS fixture + Late item${GUID}${pubDate} + `, + { headers: { 'Content-Type': 'application/rss+xml' } } + ) +} + +describe('RSS delivery across delayed feed updates', () => { + beforeEach(() => { + vi.clearAllMocks() + mockValidateUrl.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.1' }) + mockProcessEvent.mockResolvedValue({ success: true }) + mockUpdateConfig.mockResolvedValue(undefined) + }) + + it('delivers an unseen item published before the last poll but after subscription', async () => { + mockFetch.mockResolvedValue(feed('Fri, 11 Sep 2026 21:25:32 GMT')) + + expect(await rssPollingHandler.pollWebhook(context())).toBe('success') + expect(mockProcessEvent).toHaveBeenCalledExactlyOnceWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ item: expect.objectContaining({ guid: GUID }) }), + 'rss-request' + ) + expect(mockUpdateConfig).toHaveBeenCalledWith( + 'rss-webhook', + expect.objectContaining({ lastSeenGuids: [GUID] }), + expect.anything() + ) + }) + + it('does not redeliver a known GUID when its publication date changes', async () => { + mockFetch.mockResolvedValue(feed('Fri, 11 Sep 2026 23:28:00 GMT')) + + expect(await rssPollingHandler.pollWebhook(context([GUID]))).toBe('success') + expect(mockProcessEvent).not.toHaveBeenCalled() + }) + + it('does not backfill items published before the subscription existed', async () => { + mockFetch.mockResolvedValue(feed('Thu, 27 Aug 2026 18:30:00 GMT')) + + expect(await rssPollingHandler.pollWebhook(context())).toBe('success') + expect(mockProcessEvent).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/webhooks/polling/rss.ts b/apps/sim/lib/webhooks/polling/rss.ts index 1fd4bb0affb..491d7756cd6 100644 --- a/apps/sim/lib/webhooks/polling/rss.ts +++ b/apps/sim/lib/webhooks/polling/rss.ts @@ -109,7 +109,7 @@ export const rssPollingHandler: PollingProviderHandler = { items: newItems, etag, lastModified, - } = await fetchNewRssItems(config, requestId, logger) + } = await fetchNewRssItems(config, webhookData.createdAt, requestId, logger) if (!newItems.length) { await updateRssState(webhookId, now.toISOString(), [], config, logger, etag, lastModified) @@ -195,6 +195,7 @@ async function updateRssState( async function fetchNewRssItems( config: RssWebhookConfig, + subscriptionStartedAt: Date, requestId: string, logger: Logger ): Promise<{ feed: RssFeed; items: RssItem[]; etag?: string; lastModified?: string }> { @@ -248,9 +249,6 @@ async function fetchNewRssItems( return { feed: feed as RssFeed, items: [], etag: newEtag, lastModified: newLastModified } } - const lastCheckedTime = config.lastCheckedTimestamp - ? new Date(config.lastCheckedTimestamp) - : null const lastSeenGuids = new Set(config.lastSeenGuids || []) const newItems = feed.items.filter((item) => { @@ -263,9 +261,13 @@ async function fetchNewRssItems( return false } - if (lastCheckedTime && item.isoDate) { + /** + * A cached feed can reveal an item after its publication time. Only the fixed + * subscription boundary excludes history; the last poll time is not a delivery cursor. + */ + if (item.isoDate) { const itemDate = new Date(item.isoDate) - if (itemDate <= lastCheckedTime) { + if (itemDate <= subscriptionStartedAt) { return false } } From 1d4c004616c491bbb6fb98b55364c60e57f27dac Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 11 Sep 2026 17:08:40 -0700 Subject: [PATCH 05/15] fix(sidebar): unify navigation menus and stabilize hydration (#7791) * fix(sidebar): unify navigation menus and stabilize hydration * fix(sidebar): simplify organization profile controls --- .../organization-footer.test.tsx | 36 ++- .../organization-footer.tsx | 248 ++---------------- .../organization-header.test.tsx | 48 +++- .../organization-header.tsx | 25 +- .../workspace-list.test.tsx | 28 ++ .../workspaces-section/workspace-list.tsx | 25 +- .../organization-sidebar.tsx | 6 + .../settings/components/general/general.tsx | 21 +- .../components/help-modal/help-modal.tsx | 6 +- .../sidebar-footer/sidebar-footer.test.tsx | 180 +++++++------ .../sidebar-footer/sidebar-footer.tsx | 176 ++++--------- .../view-invitations-menu-item.tsx | 7 +- .../workspace-header.test.tsx | 198 ++++++++++++-- .../workspace-header/workspace-header.tsx | 161 ++++++------ .../w/components/sidebar/sidebar.tsx | 43 ++- apps/sim/lib/auth/sign-out.test.ts | 46 ++++ apps/sim/lib/auth/sign-out.ts | 21 ++ .../lib/workspaces/organization-navigation.ts | 11 + .../dropdown-menu/dropdown-menu.tsx | 132 ++++++---- 19 files changed, 779 insertions(+), 639 deletions(-) create mode 100644 apps/sim/lib/auth/sign-out.test.ts create mode 100644 apps/sim/lib/auth/sign-out.ts create mode 100644 apps/sim/lib/workspaces/organization-navigation.ts diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.test.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.test.tsx index 8a39e94a9fe..0d1077648d7 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.test.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.test.tsx @@ -5,19 +5,27 @@ import { act, type ComponentProps } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockNavigate, mockPush } = vi.hoisted(() => ({ +const { mockNavigate, mockPush, context } = vi.hoisted(() => ({ mockNavigate: vi.fn(), mockPush: vi.fn(), + context: { + organization: { id: 'org-1' }, + }, })) vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush }), + usePathname: () => '/o/org-1/home', })) vi.mock('next/link', () => ({ default: ({ onNavigate, + prefetch: _prefetch, ...props - }: ComponentProps<'a'> & { onNavigate?: (event: { preventDefault: () => void }) => void }) => ( + }: ComponentProps<'a'> & { + prefetch?: boolean + onNavigate?: (event: { preventDefault: () => void }) => void + }) => ( ({ /> ), })) +vi.mock('@/lib/auth/sign-out', () => ({ signOutAndRedirect: vi.fn() })) vi.mock('@/lib/desktop', () => ({ getDesktopUpdates: () => null })) vi.mock('@/hooks/use-desktop-update-state', () => ({ useDesktopUpdateState: () => ({ status: 'idle' }), @@ -42,12 +51,14 @@ vi.mock('@/hooks/queries/user-profile', () => ({ useUserProfile: () => ({ data: { id: 'user-1', name: 'Ada', email: 'ada@example.com' } }), })) vi.mock('@/app/o/[organizationId]/providers/organization-provider', () => ({ - useOrganizationContext: () => ({ organization: { id: 'org-1' } }), + useOrganizationContext: () => context, })) vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/components', () => ({ SidebarTooltip: ({ children }: { children: React.ReactNode }) => children, })) -vi.mock('@/components/icons', () => ({ SlackIcon: () => })) +vi.mock('@/components/icons', () => ({ + SlackIcon: () => , +})) import { OrganizationFooter } from '@/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer' import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' @@ -71,7 +82,7 @@ afterEach(async () => { vi.unstubAllGlobals() }) -async function selectSettings() { +async function openProfileMenu() { await act(async () => { root.render( {}} onJoinSlack={() => {}} + onContactSupport={() => {}} /> ) }) @@ -88,15 +100,27 @@ async function selectSettings() { await act(async () => { trigger.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 })) }) +} + +async function selectSettings() { + await openProfileMenu() const link = document.querySelector('a[href="/o/org-1/settings/general"]') if (!link) throw new Error('Settings link is missing') await act(async () => link.click()) } describe('OrganizationFooter settings navigation', () => { + it('keeps only Settings and Sign out in the organization profile menu', async () => { + await openProfileMenu() + expect( + [...document.querySelectorAll('[role="menuitem"]')].map((item) => item.textContent) + ).toEqual(['Settings', 'Sign out']) + expect(document.querySelector('[role="separator"]')).toBeNull() + }) + it('navigates immediately when settings are clean', async () => { await selectSettings() - expect(mockNavigate).toHaveBeenCalledWith('/o/org-1/settings/general') + expect(mockPush).toHaveBeenCalledWith('/o/org-1/settings/general') expect(useSettingsDirtyStore.getState().pendingLeave).toBeNull() }) diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.tsx index ecc37a12e01..125ab4202af 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-footer/organization-footer.tsx @@ -1,242 +1,28 @@ 'use client' -import type { DesktopUpdateState } from '@sim/desktop-bridge' -import { - Chip, - chipContentLabelClass, - chipPrimaryFillTokens, - chipVariants, - cn, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuItemLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, - OverflowText, - Skeleton, -} from '@sim/emcn' -import { BookOpen, Download, HelpCircle, Settings } from '@sim/emcn/icons' -import { SlackIcon } from '@/components/icons' -import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link' -import { getDesktopUpdates } from '@/lib/desktop' +import type { ComponentProps } from 'react' +import { useRouter } from 'next/navigation' import { organizationRoutes } from '@/lib/navigation/paths' -import { getUserColor } from '@/lib/workspaces/colors' import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider' -import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components' -import { - SIDEBAR_ITEM_GAP_CLASS, - SIDEBAR_RAIL_CHIP_CLASS, -} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' -import { useUserProfile } from '@/hooks/queries/user-profile' -import { useDesktopUpdateState } from '@/hooks/use-desktop-update-state' +import { SidebarFooter } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer' -function hasAvailableDesktopUpdate(state: DesktopUpdateState): boolean { - return state.status === 'available' || state.status === 'downloading' || state.status === 'ready' -} - -function desktopUpdateActionLabel(state: DesktopUpdateState): string { - if (state.status === 'downloading') { - return state.percent === undefined - ? 'Downloading update…' - : `Downloading update ${state.percent}%` - } - return state.status === 'ready' ? 'Restart to update' : 'Update' -} - -/** Compact primary update circle using the same footprint as the surrounding sidebar icons. */ -function DesktopUpdateIcon({ className }: { className?: string }) { - return ( -
- {/* Download's default viewBox is asymmetric around its paths. Center the - artwork itself, not merely its SVG box, inside the avatar-sized circle. */} - -
- ) -} +interface OrganizationFooterProps + extends Omit< + ComponentProps, + 'accountSettingsHref' | 'onOpenAccountSettings' | 'navigationLinks' + > {} -interface OrganizationFooterProps { - /** - * True while the scroll region above still hides rows beyond its bottom edge — - * the same test the divider under the pinned nav applies at the top. The bar's - * top rule is drawn only then, so a list that fits meets the footer with no line. - */ - showDivider: boolean - isCollapsed: boolean - showCollapsedTooltips: boolean - onOpenDocs: () => void - onJoinSlack: () => void -} - -/** - * Pinned bottom bar of the organization sidebar: the viewer's avatar and name, - * which open their account settings, plus a help menu. Same two elements and the - * same layout as the workspace footer — expanded they share one row with help hard - * right, collapsed they stack as icon chips with help on top. - * - * Collapsed reverses the flex direction instead of reordering the DOM, which keeps - * both elements (and the help menu's trigger) alive across a toggle. - */ -export function OrganizationFooter({ - showDivider, - isCollapsed, - showCollapsedTooltips, - onOpenDocs, - onJoinSlack, -}: OrganizationFooterProps) { +export function OrganizationFooter(props: OrganizationFooterProps) { const { organization } = useOrganizationContext() - const { data: profile } = useUserProfile() - const updateState = useDesktopUpdateState() - - const name = profile ? profile.name?.trim() || profile.email : '' - const updateAvailable = hasAvailableDesktopUpdate(updateState) - - const handleUpdateSelect = () => { - const updates = getDesktopUpdates() - if (updateState.status === 'ready') { - updates?.install() - } else if (updateState.status === 'available') { - updates?.check() - } - } - - /** - * Plain `img`/`div` rather than the emcn `Avatar`, whose Radix root renders a - * `` — and globals fade every `span` in the collapsed rail to `opacity: 0`, - * which would blank the avatar exactly where it is the only thing left to see. - */ - const avatar = !profile ? ( - - ) : profile.image ? ( - - ) : ( -
- {name.charAt(0).toUpperCase()} -
- ) - - /** - * Expanded, the chip hugs its content (`max-w-full` so a long name truncates - * rather than overflowing); collapsed, `fullWidth` fills the narrow rail and - * `min-w-0` lets the hidden label give up its box so the chip never overflows it. - * The name is the button's accessible name — no `aria-label`, which would - * override the visible text. - */ - const profileMenu = ( - - - - - - - - - - - - - - - - ) - - /** - * One node across both states; only `fullWidth` changes, so the same Radix menu - * survives the transition. `shrink-0` keeps the chip off the avatar while the rail - * is briefly narrower than the row — the aside's clip hides it until there is room. - */ - const helpMenu = ( - - - - - - - {/* Anchored to whichever edge the trigger sits on, so the menu never overhangs the rail. */} - - {updateAvailable && ( - <> - - - {desktopUpdateActionLabel(updateState)} - - - - )} - - - Docs - - - - Join Slack - - - - ) + const router = useRouter() + const accountSettingsHref = organizationRoutes(organization.id).settingsSection('general') return ( -
- {/* Expanded, claims the row's free width so the help button lands hard right. - `flex` makes the inline-flex chip a flex item, so the wrapper is exactly the - chip's 30px rather than a line box padded by the strut's half-leading. */} -
{profileMenu}
- {helpMenu} -
+ router.push(accountSettingsHref)} + navigationLinks={[]} + /> ) } diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx index 665eea639dc..e3d69feee22 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.test.tsx @@ -7,7 +7,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const mocks = vi.hoisted(() => ({ upload: vi.fn(), refresh: vi.fn() })) +const mocks = vi.hoisted(() => ({ upload: vi.fn(), refresh: vi.fn(), invite: vi.fn() })) +vi.mock('@/app/workspace/[workspaceId]/components/invite-modal', () => ({ + InviteModal: (props: object) => { + mocks.invite(props) + return null + }, +})) vi.mock('@/lib/uploads/client/session-upload', () => ({ uploadInternalFileSession: mocks.upload, })) @@ -49,7 +55,12 @@ afterEach(async () => { vi.unstubAllGlobals() }) -async function render(canEditLogo = true, isCollapsed = false, onExpandSidebar = vi.fn()) { +async function render( + canEditLogo = true, + isCollapsed = false, + onExpandSidebar = vi.fn(), + canInviteMembers = canEditLogo +) { await act(async () => { root.render( @@ -57,6 +68,7 @@ async function render(canEditLogo = true, isCollapsed = false, onExpandSidebar = @@ -189,3 +201,35 @@ describe('OrganizationHeader logo upload', () => { expect(mocks.refresh).toHaveBeenCalledOnce() }) }) + +describe('OrganizationHeader member actions', () => { + it('opens the existing invitation flow for the current organization', async () => { + await render() + await openMenu() + const invite = [...document.querySelectorAll('[role="menuitem"]')].find( + (item) => item.textContent === 'Invite people' + )! + await act(async () => invite.click()) + expect(mocks.invite).toHaveBeenCalledWith( + expect.objectContaining({ + open: true, + organizationId: 'org-1', + isOrganizationAdmin: true, + canInvite: true, + }) + ) + }) + + it.each([false, true])( + 'keeps settings access but hides disallowed invitations (admin=%s)', + async (admin) => { + await render(admin, false, vi.fn(), false) + await openMenu() + expect(document.querySelector('a[href="/o/org-1/settings/members"]')).toHaveTextContent( + 'Settings' + ) + expect(document.querySelector('[role="menu"]')).not.toHaveTextContent('Invite people') + expect(mocks.invite).not.toHaveBeenCalled() + } + ) +}) diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx index 19a580ae22b..3075628dbc7 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/organization-header/organization-header.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRef } from 'react' +import { useRef, useState } from 'react' import { Chip, ChipChevronDown, @@ -12,13 +12,14 @@ import { Tooltip, toast, } from '@sim/emcn' -import { PanelLeft, Settings } from '@sim/emcn/icons' +import { PanelLeft, Send, Settings } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' import { IdentityTile } from '@/components/identity-tile/identity-tile' import { getOrganizationSettingsHref } from '@/components/settings/navigation' import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link' import type { OrganizationSurfaceOrganization } from '@/lib/organizations/surface' import { LOGO_ACCEPT_ATTRIBUTE } from '@/lib/uploads/client/logo-file' +import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal' import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' import { useUploadOrganizationLogo } from '@/hooks/queries/organization-logo' @@ -29,6 +30,7 @@ function getOrganizationInitial(name: string): string { interface OrganizationHeaderProps { organization: OrganizationSurfaceOrganization canEditLogo: boolean + canInviteMembers: boolean isCollapsed: boolean /** Expands the rail; the collapsed header is itself the expand control. */ onExpandSidebar: () => void @@ -44,6 +46,7 @@ interface OrganizationHeaderProps { export function OrganizationHeader({ organization, canEditLogo, + canInviteMembers, isCollapsed, onExpandSidebar, }: OrganizationHeaderProps) { @@ -52,6 +55,7 @@ export function OrganizationHeader({ const { mutate: uploadLogo, isPending: isUploadingLogo } = useUploadOrganizationLogo( organization.id ) + const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) const initial = getOrganizationInitial(organization.name) if (isCollapsed) { @@ -131,7 +135,7 @@ export function OrganizationHeader({ aria-label='Change organization logo' aria-busy={isUploadingLogo} textValue='Change organization logo' - className='h-auto shrink-0 p-1' + className='h-auto shrink-0 p-0 hover-hover:opacity-70 focus-visible:opacity-70' disabled={isUploadingLogo} onSelect={(event) => { event.preventDefault() @@ -161,8 +165,23 @@ export function OrganizationHeader({ Settings + {canInviteMembers && ( + setIsInviteModalOpen(true)}> + + Invite people + + )} + {isInviteModalOpen && ( + + )}
) } diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.test.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.test.tsx index e4ce1404ee2..2a08c775fb9 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.test.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.test.tsx @@ -45,6 +45,22 @@ vi.mock('@/hooks/queries/workspace', () => ({ })) import { WorkspaceList } from '@/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list' +import { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu' + +function KeyboardWorkspaceFlyout() { + const hover = useHoverMenu() + return ( + (open ? hover.open() : hover.close())} + > + Workspaces + + + + + ) +} let container: HTMLDivElement let root: Root @@ -91,6 +107,18 @@ async function render() { } describe('WorkspaceList rail view', () => { + it('keeps the flyout open when opened from the keyboard without pointer hover', async () => { + workspacesState.workspaces = [{ id: 'ws-1', name: 'Design' }] + await act(async () => root.render()) + const trigger = container.querySelector('button')! + await act(async () => { + trigger.focus() + trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + }) + expect(document.querySelector('[role="menu"]')).not.toBeNull() + expect(document.querySelector('a[href="/workspace/ws-1"]')).toHaveTextContent('Design') + }) + it('lists every workspace as a link into it', async () => { workspacesState.workspaces = [ { id: 'ws-1', name: 'Design' }, diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx index c0085b6b92c..8d1bfa0b942 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx @@ -52,11 +52,13 @@ export function WorkspaceList({ organizationId, pathname, flyout }: WorkspaceLis }, }) const lockFlyout = flyout?.setLocked + const isInteracting = menu.isOpen || rename.editingId !== null useEffect(() => { - lockFlyout?.(menu.isOpen || rename.editingId !== null) - return () => lockFlyout?.(false) - }, [lockFlyout, menu.isOpen, rename.editingId]) + if (!lockFlyout || !isInteracting) return + lockFlyout(true) + return () => lockFlyout(false) + }, [lockFlyout, isInteracting]) const visibleWorkspaces = flyout ? workspaces : workspaces.slice(0, visibleCount) const hasMore = workspaces.length > visibleCount @@ -90,7 +92,11 @@ export function WorkspaceList({ organizationId, pathname, flyout }: WorkspaceLis initial={getWorkspaceInitial(workspace.name)} logoUrl={workspace.logoUrl} /> - + ) const onMoreClick = (event: React.MouseEvent) => { @@ -136,6 +142,16 @@ export function WorkspaceList({ organizationId, pathname, flyout }: WorkspaceLis onPointerMove={(event) => { if (menu.isOpen || rename.editingId) event.preventDefault() }} + actionIndicator={ + isPinned ? ( + + ) : undefined + } action={ openMenu(event, workspace.id)} > {label} - {isPinned && } ) diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx index 363e229f6ec..c4434335f55 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/organization-sidebar.tsx @@ -27,6 +27,7 @@ import { useSidebarChrome } from '@/app/workspace/[workspaceId]/components/works import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { + HelpModal, isNavItemActive, NavItemContextMenu, SidebarNavChip, @@ -94,6 +95,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() { const settingsPath = organizationRoutes(organization.id).settings const isSettings = pathname === settingsPath || pathname?.startsWith(`${settingsPath}/`) + const [isHelpModalOpen, setIsHelpModalOpen] = useState(false) const [menuHref, setMenuHref] = useState(null) const { isOpen: isHrefMenuOpen, @@ -182,6 +184,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() { @@ -282,6 +285,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() { showCollapsedTooltips={showCollapsedTooltips} onOpenDocs={handleOpenDocs} onJoinSlack={handleOpenSlackCommunity} + onContactSupport={() => setIsHelpModalOpen(true)} /> + + {/* Not on the peek card: the resize hook writes an inline `--sidebar-width` that out-specifies the `[data-peek]` rule, stranding the card at a stale width. */} {!isPeeking && ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx index 9ffff5c4db5..bc7e01d96b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx @@ -23,8 +23,9 @@ import dynamic from 'next/dynamic' import Image from 'next/image' import { useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' -import { signOut, useSession } from '@/lib/auth/auth-client' +import { useSession } from '@/lib/auth/auth-client' import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' +import { signOutAndRedirect } from '@/lib/auth/sign-out' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' import { getBrowserTimezone, getTimezoneOptions } from '@/lib/core/utils/timezone' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -49,7 +50,6 @@ import { useUpdateUserProfile, useUserProfile, } from '@/hooks/queries/user-profile' -import { clearUserData } from '@/stores' const AuthorizedApps = dynamic(() => import('@/app/workspace/[workspaceId]/settings/components/authorized-apps/authorized-apps').then( @@ -195,21 +195,6 @@ export function General() { handleUpdateName() } - const handleSignOut = async () => { - const logoutUrl = '/login?fromLogout=true' - let canNavigateInApp = false - - try { - const [, inMemoryResetSucceeded] = await Promise.all([signOut(), clearUserData()]) - canNavigateInApp = inMemoryResetSucceeded - } catch (error) { - logger.error('Error signing out:', { error }) - } - - if (canNavigateInApp) router.push(logoutUrl) - else window.location.assign(logoutUrl) - } - const handleResetPasswordConfirm = async () => { if (!profile?.email) return @@ -299,7 +284,7 @@ export function General() { : []), ...(session?.user?.id && !isAuthDisabled ? [ - { id: 'sign-out', text: 'Sign out', onSelect: handleSignOut }, + { id: 'sign-out', text: 'Sign out', onSelect: () => signOutAndRedirect(router.push) }, { id: 'reset-password', text: 'Reset password', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx index 6e734ec5c31..635ffebb475 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx @@ -53,14 +53,14 @@ interface HelpModalProps { open: boolean onOpenChange: (open: boolean) => void workflowId?: string - workspaceId: string + workspaceId?: string } interface SubmitHelpVariables { data: FormValues images: ImageWithPreview[] workflowId?: string - workspaceId: string + workspaceId?: string } async function compressImage(file: File): Promise { @@ -93,7 +93,7 @@ async function submitHelpRequest({ data, images, workflowId, workspaceId }: Subm formData.append('subject', data.subject) formData.append('message', data.message) formData.append('type', data.type) - formData.append('workspaceId', workspaceId) + if (workspaceId) formData.append('workspaceId', workspaceId) formData.append('userAgent', navigator.userAgent) if (workflowId) { formData.append('workflowId', workflowId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.test.tsx index 22d0f09b014..c4455bac823 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.test.tsx @@ -1,10 +1,10 @@ /** * @vitest-environment jsdom */ -import { act } from 'react' -import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { act, type ComponentProps } from 'react' +import { Building, Credit, Trash, Users } from '@sim/emcn/icons' import { createRoot, type Root } from 'react-dom/client' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const desktopMocks = vi.hoisted(() => ({ getState: vi.fn(), @@ -15,15 +15,26 @@ const desktopMocks = vi.hoisted(() => ({ unsubscribe: vi.fn(), })) -const { hostContext } = vi.hoisted(() => ({ - hostContext: { - hostOrganizationId: null as string | null, - viewer: { isHostOrganizationMember: false }, - features: { - organizationSearch: undefined as boolean | undefined, - knowledgeMemberAccess: false, - }, - }, +const authMocks = vi.hoisted(() => ({ signOut: vi.fn(), userId: 'user-1' })) +vi.mock('@/lib/auth/sign-out', () => ({ signOutAndRedirect: authMocks.signOut })) +vi.mock('next/link', () => ({ + default: ({ + onNavigate, + prefetch: _prefetch, + ...props + }: ComponentProps<'a'> & { + prefetch?: boolean + onNavigate?: (event: { preventDefault: () => void }) => void + }) => ( + { + event.preventDefault() + onNavigate?.({ preventDefault: () => {} }) + }} + /> + ), })) vi.mock('@/lib/desktop', () => ({ @@ -35,23 +46,11 @@ vi.mock('@/lib/desktop', () => ({ }), })) vi.mock('@/hooks/queries/user-profile', () => ({ - useUserProfile: () => ({ data: { id: 'user-1', name: 'Ada', email: 'ada@sim.ai' } }), + useUserProfile: () => ({ data: { id: authMocks.userId, name: 'Ada', email: 'ada@sim.ai' } }), })) -vi.mock('@/lib/auth/auth-client', () => ({ - useSession: () => ({ data: { user: { id: 'user-1' } } }), -})) -vi.mock('@/lib/billing/workspace-permissions', () => ({ - canViewWorkspaceBillingSettings: () => true, -})) -/** Billing routes the invitations-disabled row to Subscription; read at render time. */ -beforeAll(() => setEnvFlags({ isBillingEnabled: true })) -afterAll(resetEnvFlagsMock) -vi.mock('@/lib/workspaces/colors', () => ({ getUserColor: () => '#000000' })) -vi.mock('@/hooks/use-workspace-invite-policy', () => ({ - useWorkspaceInvitePolicy: () => ({ isInvitationsDisabled: false }), -})) -vi.mock('@/app/workspace/[workspaceId]/providers/workspace-host-provider', () => ({ - useWorkspaceHostContext: () => hostContext, +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: vi.fn() }), + usePathname: () => '/workspace/ws-emir/home', })) vi.mock( '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-tooltip/sidebar-tooltip', @@ -63,7 +62,9 @@ vi.mock('@/components/icons', () => ({ SlackIcon: ({ className }: { className?: string }) => , })) +import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' import { SidebarFooter } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer' +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' let container: HTMLDivElement let root: Root @@ -76,12 +77,31 @@ async function renderFooter( await act(async () => { root.render( `/workspace/workspace-1/settings/${section}`} - onOpenSettings={() => {}} + accountSettingsHref='/workspace/workspace-1/settings/general' + onOpenAccountSettings={() => {}} + navigationLinks={[ + { + label: 'Subscription', + icon: Credit, + href: '/workspace/workspace-1/settings/billing', + onNavigate: () => {}, + }, + { + label: 'Teammates', + icon: Users, + href: '/workspace/workspace-1/settings/teammates', + onNavigate: () => {}, + }, + { + label: 'Recently deleted', + icon: Trash, + href: '/workspace/workspace-1/settings/recently-deleted', + onNavigate: () => {}, + }, + ]} onOpenDocs={() => {}} onJoinSlack={() => {}} onContactSupport={() => {}} @@ -129,10 +149,8 @@ function menuItem(label: string): HTMLElement { beforeEach(() => { vi.clearAllMocks() - hostContext.hostOrganizationId = null - hostContext.viewer.isHostOrganizationMember = false - hostContext.features.organizationSearch = undefined - hostContext.features.knowledgeMemberAccess = false + authMocks.userId = 'user-1' + useSettingsDirtyStore.getState().reset() desktopMocks.listener = null desktopMocks.onState.mockImplementation((listener) => { desktopMocks.listener = listener @@ -150,50 +168,64 @@ afterEach(() => { }) describe('SidebarFooter', () => { - it('links members back to the organization hosting the current workspace', async () => { - hostContext.hostOrganizationId = 'host-org' - hostContext.viewer.isHostOrganizationMember = true - hostContext.features.organizationSearch = true + it('keeps the familiar Settings entry in the profile menu', async () => { await renderFooter({ status: 'idle' }) - openProfileMenu() + expect( + [...document.querySelectorAll('[role="menuitem"]')].map((item) => item.textContent) + ).toEqual(['Settings', 'Subscription', 'Teammates', 'Recently deleted', 'Sign out']) + expect(document.querySelector('[role="separator"]')).toBeNull() + expect(menuItem('Settings')).toHaveAttribute('href', '/workspace/workspace-1/settings/general') + }) - expect(menuItem('Organization')).toHaveAttribute('href', '/o/host-org') - const labels = Array.from(document.querySelectorAll('[role="menuitem"]')).map( - (item) => item.textContent + it('guards returning to the organization when settings are unsaved', async () => { + const onNavigate = vi.fn() + await renderFooter( + { status: 'idle' }, + { + navigationLinks: [{ label: 'Organization', icon: Building, href: '/o/org-1', onNavigate }], + } ) - expect(labels.indexOf('Organization')).toBe(labels.indexOf('Settings') + 1) - expect(labels.indexOf('Organization')).toBeLessThan(labels.indexOf('Teammates')) - expect(document.querySelector('[role="menu"] [role="separator"]')).toBeNull() + useSettingsDirtyStore.getState().setDirty(true) + openProfileMenu() + expect(menuItem('Organization')).toHaveAttribute('href', '/o/org-1') + act(() => menuItem('Organization').click()) + expect(onNavigate).not.toHaveBeenCalled() + act(() => useSettingsDirtyStore.getState().confirmLeave()) + expect(onNavigate).toHaveBeenCalledOnce() }) - it.each([false, undefined])( - 'keeps the workspace profile menu when org rollout is %s', - async (enabled) => { - hostContext.hostOrganizationId = 'host-org' - hostContext.viewer.isHostOrganizationMember = true - hostContext.features.organizationSearch = enabled - hostContext.features.knowledgeMemberAccess = true - await renderFooter({ status: 'idle' }) - - openProfileMenu() - - expect(document.querySelector('[role="menu"]')).not.toHaveTextContent('Organization') - expect(menuItem('Settings')).toHaveAttribute( - 'href', - '/workspace/workspace-1/settings/general' - ) - } - ) + it('uses the shared sign-out flow', async () => { + await renderFooter({ status: 'idle' }) + openProfileMenu() + await act(async () => menuItem('Sign out').click()) + expect(authMocks.signOut).toHaveBeenCalledOnce() + }) - it.each([null, 'host-org'])('hides Organization without host membership (%s)', async (orgId) => { - hostContext.hostOrganizationId = orgId - hostContext.features.organizationSearch = true + it('defers sign-out while settings are unsaved', async () => { await renderFooter({ status: 'idle' }) + useSettingsDirtyStore.getState().setDirty(true) + openProfileMenu() + await act(async () => menuItem('Sign out').click()) + expect(authMocks.signOut).not.toHaveBeenCalled() + act(() => useSettingsDirtyStore.getState().confirmLeave()) + expect(authMocks.signOut).toHaveBeenCalledOnce() + }) + it('hides sign-out for auth-disabled deployments', async () => { + authMocks.userId = ANONYMOUS_USER_ID + await renderFooter({ status: 'idle' }) openProfileMenu() + expect(document.querySelector('[role="menu"]')).not.toHaveTextContent('Sign out') + expect(document.querySelector('[role="separator"]')).toBeNull() + }) - expect(document.querySelector('[role="menu"]')).not.toHaveTextContent('Organization') + it('opens the shared support flow', async () => { + const onContactSupport = vi.fn() + await renderFooter({ status: 'idle' }, { onContactSupport }) + openHelpMenu() + act(() => menuItem('Contact support').click()) + expect(onContactSupport).toHaveBeenCalledOnce() }) it('keeps the overflow tooltip disabled while the collapsed tooltip still owns the trigger', async () => { @@ -212,18 +244,6 @@ describe('SidebarFooter', () => { expect(document.querySelector('[data-native-surface-overlay]')).toBeNull() }) - it('renders profile settings destinations with native link semantics', async () => { - await renderFooter({ status: 'idle' }) - - openProfileMenu() - - expect(menuItem('Settings')).toHaveAttribute('href', '/workspace/workspace-1/settings/general') - expect(menuItem('Subscription')).toHaveAttribute( - 'href', - '/workspace/workspace-1/settings/billing' - ) - }) - it('keeps the ordinary help treatment when no update is available', async () => { await renderFooter({ status: 'idle' }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx index 6963d707b56..1da14a9bf22 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx @@ -17,26 +17,14 @@ import { OverflowText, Skeleton, } from '@sim/emcn' -import { - BookOpen, - Building, - Credit, - Download, - HelpCircle, - Settings, - Trash, - Users, -} from '@sim/emcn/icons' +import { BookOpen, Download, HelpCircle, LogOut, Settings } from '@sim/emcn/icons' +import { useRouter } from 'next/navigation' import { SlackIcon } from '@/components/icons' import { SettingsIntentLink } from '@/components/settings/settings-intent-link' -import { useSession } from '@/lib/auth/auth-client' -import { canViewWorkspaceBillingSettings } from '@/lib/billing/workspace-permissions' -import { useDeploymentShape } from '@/lib/core/config/deployment-shape' +import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' +import { signOutAndRedirect } from '@/lib/auth/sign-out' import { getDesktopUpdates } from '@/lib/desktop' -import { organizationRoutes } from '@/lib/navigation/paths' import { getUserColor } from '@/lib/workspaces/colors' -import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' -import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-tooltip' import { SIDEBAR_ITEM_GAP_CLASS, @@ -44,28 +32,7 @@ import { } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' import { useUserProfile } from '@/hooks/queries/user-profile' import { useDesktopUpdateState } from '@/hooks/use-desktop-update-state' -import { useWorkspaceInvitePolicy } from '@/hooks/use-workspace-invite-policy' - -/** - * Settings destinations reachable from the profile menu, in display order. Labels - * and icons mirror the settings navigation entries they open, so the menu and the - * settings sidebar never disagree about what a section is called. - * - * Which of them a given viewer actually gets is decided in {@link SidebarFooter} — - * the same gates the settings sidebar and the section route apply, so the menu - * never lists a page the server would refuse. - */ -const PROFILE_MENU_ITEMS: readonly { - section: SettingsSection | 'organization' - label: string - icon: ComponentType<{ className?: string }> -}[] = [ - { section: 'general', label: 'Settings', icon: Settings }, - { section: 'organization', label: 'Organization', icon: Building }, - { section: 'billing', label: 'Subscription', icon: Credit }, - { section: 'teammates', label: 'Teammates', icon: Users }, - { section: 'recently-deleted', label: 'Recently deleted', icon: Trash }, -] +import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' function hasAvailableDesktopUpdate(state: DesktopUpdateState): boolean { return state.status === 'available' || state.status === 'downloading' || state.status === 'ready' @@ -97,8 +64,14 @@ function DesktopUpdateIcon({ className }: { className?: string }) { ) } +interface SidebarNavigationLink { + label: string + icon: ComponentType<{ className?: string }> + href: string + onNavigate: () => void +} + interface SidebarFooterProps { - workspaceId: string /** * True while the scroll region above still hides rows beyond its bottom edge — * the same test the divider under the pinned nav applies at the top. The bar's @@ -107,16 +80,16 @@ interface SidebarFooterProps { showDivider: boolean isCollapsed: boolean showCollapsedTooltips: boolean - getSettingsHref: (section: SettingsSection) => string - onOpenSettings: (section: SettingsSection) => void + accountSettingsHref: string + onOpenAccountSettings: () => void + navigationLinks: readonly SidebarNavigationLink[] onOpenDocs: () => void onJoinSlack: () => void onContactSupport: () => void } /** - * Pinned bottom bar of the workspace sidebar: the viewer's avatar and name, which - * open a menu of their settings destinations, plus a help menu. + * Shared account and Help menus for the workspace and organization sidebars. * * Expanded, the two share one row — the profile claims the free width so the help * button lands hard right, mirroring the collapse control in the workspace header. @@ -138,31 +111,23 @@ interface SidebarFooterProps { * than remounting a trigger mid-animation. */ export function SidebarFooter({ - workspaceId, showDivider, isCollapsed, showCollapsedTooltips, - getSettingsHref, - onOpenSettings, + accountSettingsHref, + onOpenAccountSettings, + navigationLinks, onOpenDocs, onJoinSlack, onContactSupport, }: SidebarFooterProps) { const { data: profile } = useUserProfile() - const { data: session } = useSession() - const hostContext = useWorkspaceHostContext() - const { billingEnabled } = useDeploymentShape() - const { isInvitationsDisabled } = useWorkspaceInvitePolicy(workspaceId) + const router = useRouter() const updateState = useDesktopUpdateState() const name = profile ? profile.name?.trim() || profile.email : '' const updateAvailable = hasAvailableDesktopUpdate(updateState) - const organizationHref = - hostContext.hostOrganizationId && - hostContext.viewer.isHostOrganizationMember && - hostContext.features?.organizationSearch - ? organizationRoutes(hostContext.hostOrganizationId).root - : null + const canSignOut = Boolean(profile && profile.id !== ANONYMOUS_USER_ID) const handleUpdateSelect = () => { const updates = getDesktopUpdates() @@ -173,34 +138,6 @@ export function SidebarFooter({ } } - /** - * Subscription is dropped for viewers the Billing page would turn away — a - * deployment with billing off, or anyone who is not the payer (on an - * organization-hosted workspace, every member who is not an org admin). The - * settings sidebar hides its own Billing entry on exactly this test. - */ - const menuItems = PROFILE_MENU_ITEMS.filter( - (item) => - (item.section !== 'organization' || Boolean(organizationHref)) && - (item.section !== 'billing' || - canViewWorkspaceBillingSettings(hostContext, session?.user?.id)) - ) - - /** - * Teammates is a dead end on a plan that cannot invite, so a blocked viewer is - * sent to the plan itself instead — which resolves to the upgrade page for - * anyone who cannot manage the payer. With billing off there is nowhere to send - * them and no upgrade to make, so the row simply does nothing. This is the gate - * the workspace switcher's "Manage workspace" entry carried before this menu - * took the section over. - */ - const resolveMenuDestination = (section: SettingsSection): SettingsSection | null => { - if (section === 'teammates' && isInvitationsDisabled) { - return billingEnabled ? 'billing' : null - } - return section - } - /** * Built from plain `img`/`div` rather than the emcn `Avatar`, whose Radix root * renders a `` — and globals fade every `span` in the collapsed rail to @@ -276,43 +213,40 @@ export function SidebarFooter({ - {menuItems.map(({ section, label, icon: Icon }) => { - if (section === 'organization') { - if (!organizationHref) return null - return ( - - - - - - - ) - } - const destination = resolveMenuDestination(section) - if (!destination) { - return ( - - - {label} - - ) - } - - return ( - - { - event.preventDefault() - onOpenSettings(destination) - }} - > - - - - - ) - })} + {[ + { + label: 'Settings', + icon: Settings, + href: accountSettingsHref, + onNavigate: onOpenAccountSettings, + }, + ...navigationLinks, + ].map(({ label, icon: Icon, href, onNavigate }) => ( + + { + event.preventDefault() + useSettingsDirtyStore.getState().requestLeave(onNavigate) + }} + > + + + + + ))} + {canSignOut && ( + { + useSettingsDirtyStore.getState().requestLeave(() => { + void signOutAndRedirect(router.push) + }) + }} + > + + Sign out + + )} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx index c0d1e12e5c2..0983e191dca 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx @@ -1,6 +1,6 @@ 'use client' -import { Chip } from '@sim/emcn' +import { DropdownMenuItem } from '@sim/emcn' import { Mail } from '@sim/emcn/icons' import { useMyPendingInvitations } from '@/hooks/queries/invitations' @@ -22,8 +22,9 @@ export function ViewInvitationsMenuItem({ onOpen }: ViewInvitationsMenuItemProps } return ( - + + View invitations - + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.test.tsx index bcef7009e6a..2ba35bebcef 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.test.tsx @@ -3,21 +3,37 @@ */ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' +import { renderToString } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockNavigateToSettings, mockWorkspacePermissions } = vi.hoisted(() => ({ +const { mockNavigateToSettings, mockWorkspacePermissions, hostContext } = vi.hoisted(() => ({ mockNavigateToSettings: vi.fn(), + hostContext: { + hostOrganizationId: null as string | null, + viewer: { isHostOrganizationMember: false }, + features: { organizationSearch: false as boolean | undefined }, + }, mockWorkspacePermissions: { canAdmin: true, canEdit: true, canRead: true }, })) +vi.mock('@/app/workspace/[workspaceId]/providers/workspace-host-provider', () => ({ + useWorkspaceHostContext: () => hostContext, +})) + const onWorkspaceSwitch = vi.fn() vi.mock('@tanstack/react-query', () => ({ useQueryClient: () => ({ invalidateQueries: vi.fn(), setQueryData: vi.fn() }), })) +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: vi.fn() }), + usePathname: () => '/workspace/ws-emir/home', +})) vi.mock('@/lib/auth/auth-client', () => ({ useActiveOrganization: () => ({ data: null }) })) vi.mock('@/hooks/use-settings-navigation', () => ({ - useSettingsNavigation: () => ({ navigateToSettings: mockNavigateToSettings }), + useSettingsNavigation: () => ({ + navigateToSettings: mockNavigateToSettings, + }), })) vi.mock('@/hooks/use-permission-config', () => ({ usePermissionConfig: () => ({ isInvitationsDisabled: false }), @@ -89,31 +105,35 @@ function render(overrides: Partial[0]> = {}) document.body.appendChild(container) root = createRoot(container) act(() => { - root.render( - {}} - isWorkspacesLoading={false} - isCreatingWorkspace={false} - isWorkspaceMenuOpen - setIsWorkspaceMenuOpen={() => {}} - onWorkspaceSwitch={onWorkspaceSwitch} - onCreateWorkspace={async () => {}} - onRenameWorkspace={async () => {}} - onDeleteWorkspace={async () => {}} - isDeletingWorkspace={false} - onUploadLogo={() => {}} - onLeaveWorkspace={async () => {}} - isLeavingWorkspace={false} - {...overrides} - /> - ) + root.render(header(overrides)) }) } +function header(overrides: Partial[0]> = {}) { + return ( + {}} + isWorkspacesLoading={false} + isCreatingWorkspace={false} + isWorkspaceMenuOpen + setIsWorkspaceMenuOpen={() => {}} + onWorkspaceSwitch={onWorkspaceSwitch} + onCreateWorkspace={async () => {}} + onRenameWorkspace={async () => {}} + onDeleteWorkspace={async () => {}} + isDeletingWorkspace={false} + onUploadLogo={() => {}} + onLeaveWorkspace={async () => {}} + isLeavingWorkspace={false} + {...overrides} + /> + ) +} + function row(name: string): HTMLElement { const found = [...document.querySelectorAll('[data-workspace-row-idx]')].find((el) => el.textContent?.includes(name) @@ -150,6 +170,9 @@ function typeInto(input: HTMLInputElement, value: string) { beforeEach(() => { vi.clearAllMocks() + hostContext.hostOrganizationId = null + hostContext.viewer.isHostOrganizationMember = false + hostContext.features.organizationSearch = false Object.assign(mockWorkspacePermissions, { canAdmin: true, canEdit: true, canRead: true }) // jsdom implements neither; the component scrolls the active row into view. Element.prototype.scrollIntoView = vi.fn() @@ -161,6 +184,80 @@ afterEach(() => { }) describe('WorkspaceHeader workspace switcher highlight', () => { + it.each([false, true])( + 'renders prefetched workspace identity before hydration (collapsed: %s)', + (isCollapsed) => { + render({ isWorkspaceMenuOpen: false, isCollapsed }) + const html = renderToString(header({ isWorkspaceMenuOpen: false, isCollapsed })) + expect(html).toContain(isCollapsed ? 'Expand sidebar' : 'Switch workspace') + expect(html).not.toContain('animate-pulse') + } + ) + + it.each([5, 6])('only shows search once there are six workspaces: %i', (count) => { + render({ workspaces: WORKSPACES.slice(0, count) }) + expect(Boolean(document.querySelector('input[placeholder="Search workspaces..."]'))).toBe( + count === 6 + ) + }) + + it('handles a no-match query without navigating and restores rows when cleared', () => { + render() + const search = document.querySelector( + 'input[placeholder="Search workspaces..."]' + )! + act(() => typeInto(search, 'no-such-workspace')) + expect(document.querySelectorAll('[data-workspace-row-idx]')).toHaveLength(0) + expect(document.body).toHaveTextContent('No results for "no-such-workspace"') + act(() => search.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) + expect(onWorkspaceSwitch).not.toHaveBeenCalled() + + act(() => typeInto(search, ' ACME ')) + expect(document.querySelectorAll('[data-workspace-row-idx]')).toHaveLength(1) + expect(isMarked('Acme')).toBe(true) + act(() => typeInto(search, '')) + expect(document.querySelectorAll('[data-workspace-row-idx]')).toHaveLength(WORKSPACES.length) + }) + + it('keeps search focused when the pointer crosses a workspace row', () => { + render() + const search = document.querySelector( + 'input[placeholder="Search workspaces..."]' + )! + act(() => search.focus()) + const item = row('RVT').querySelector('[role="menuitem"]')! + const pointerMove = new MouseEvent('pointermove', { bubbles: true, cancelable: true }) + Object.defineProperty(pointerMove, 'pointerType', { value: 'mouse' }) + act(() => item.dispatchEvent(pointerMove)) + expect(document.activeElement).toBe(search) + const pointerOut = new MouseEvent('pointerout', { bubbles: true, cancelable: true }) + Object.defineProperty(pointerOut, 'pointerType', { value: 'mouse' }) + act(() => item.dispatchEvent(pointerOut)) + expect(document.activeElement).toBe(search) + }) + + it('keeps pinned status available to assistive technology', () => { + render({ pinnedWorkspaceIds: new Set(['ws-rvt']) }) + expect(row('RVT').querySelector('[aria-label="Pinned"]')).toHaveAttribute( + 'aria-hidden', + 'false' + ) + }) + + it('does not open creation when workspace policy disallows it', () => { + const setIsWorkspaceMenuOpen = vi.fn() + render({ + workspaceCreationPolicy: { canCreate: false, reason: 'Organization limit reached' }, + setIsWorkspaceMenuOpen, + }) + const create = [...document.querySelectorAll('[role="menuitem"]')].find( + (item) => item.textContent?.trim() === 'New workspace' + )! + expect(create).toHaveAttribute('aria-disabled', 'true') + act(() => create.click()) + expect(setIsWorkspaceMenuOpen).not.toHaveBeenCalled() + }) + it.each([ { role: 'viewer', canAdmin: false, canEdit: false }, { role: 'editor', canAdmin: false, canEdit: true }, @@ -170,14 +267,33 @@ describe('WorkspaceHeader workspace switcher highlight', () => { ({ canAdmin, canEdit }) => { Object.assign(mockWorkspacePermissions, { canAdmin, canEdit }) render() - const invite = [...document.querySelectorAll('button')].find( - (button) => button.textContent?.trim() === 'Invite teammates' + const invite = [...document.querySelectorAll('[role="menuitem"]')].find( + (item) => item.textContent?.trim() === 'Invite teammates' ) expect(Boolean(invite)).toBe(canAdmin) expect(container.querySelector('button[aria-label="Switch workspace"]')).not.toBeDisabled() } ) + it('selects a workspace through the menu keyboard interaction', () => { + render() + const item = row('RVT').querySelector('[role="menuitem"]')! + act(() => item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))) + expect(onWorkspaceSwitch).toHaveBeenCalledOnce() + expect(onWorkspaceSwitch).toHaveBeenCalledWith(WORKSPACES[0]) + }) + + it('opens workspace options without switching workspaces', () => { + render() + const options = row('RVT').querySelector('[aria-label="Workspace options"]')! + act(() => { + options.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 })) + options.click() + }) + expect(onWorkspaceSwitch).not.toHaveBeenCalled() + expect(isMarked('RVT')).toBe(true) + }) + it('shows the route workspace identity while the switcher list is unavailable', () => { render({ activeWorkspace: { name: 'Brightwave' }, @@ -286,3 +402,33 @@ describe('WorkspaceHeader workspace switcher highlight', () => { expect(isMarked("Emir's Workspace")).toBe(true) }) }) + +describe('WorkspaceHeader context navigation', () => { + it('links to the current host organization for enrolled members', () => { + hostContext.hostOrganizationId = 'host-org' + hostContext.viewer.isHostOrganizationMember = true + hostContext.features.organizationSearch = true + render() + expect(document.querySelector('a[href="/o/host-org"]')).toHaveTextContent( + 'Back to organization' + ) + }) + + it.each([ + { org: null, member: true, enabled: true }, + { org: 'host-org', member: false, enabled: true }, + { org: 'host-org', member: true, enabled: false }, + { org: 'host-org', member: true, enabled: undefined }, + ])('hides inaccessible organization navigation: %j', ({ org, member, enabled }) => { + hostContext.hostOrganizationId = org + hostContext.viewer.isHostOrganizationMember = member + hostContext.features.organizationSearch = enabled + render() + expect(document.querySelector('a[href^="/o/"]')).toBeNull() + }) + + it('keeps settings in the profile menu instead of duplicating it in the switcher', () => { + render() + expect(document.querySelector('a[href="/workspace/ws-emir/settings/teammates"]')).toBeNull() + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index c4bf57a223a..e70913f0ba5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -8,15 +8,15 @@ import { ChipInput, chipContentLabelClass, chipGeometryClass, - chipVariants, cn, DropdownMenu, DropdownMenuContent, + DropdownMenuItem, + DropdownMenuItemAction, + DropdownMenuItemLabel, DropdownMenuSeparator, DropdownMenuTrigger, OverflowText, - Plus, - Send, Skeleton, scrollFadeAttributes, scrollFadeClass, @@ -24,16 +24,19 @@ import { toast, useScrollEdges, } from '@sim/emcn' -import { MoreHorizontal, PanelLeft, Pin, Search } from '@sim/emcn/icons' +import { ArrowLeft, MoreHorizontal, PanelLeft, Pin, Plus, Search, Send } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { IdentityTile } from '@/components/identity-tile/identity-tile' +import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link' import { WorkspaceContextMenu } from '@/components/workspaces/workspace-context-menu' import { useDeploymentShape } from '@/lib/core/config/deployment-shape' import { WORKSPACE_SEARCH_THRESHOLD } from '@/lib/workspaces/constants' import { getWorkspaceInitial } from '@/lib/workspaces/initials' +import { getWorkspaceOrganizationHref } from '@/lib/workspaces/organization-navigation' import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal' +import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row' import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal' @@ -65,7 +68,9 @@ function DisabledReasonTooltip({ reason, children }: DisabledReasonTooltipProps) if (!reason) return children return ( - {children} + +
{children}
+

{reason}

@@ -177,6 +182,9 @@ function WorkspaceHeaderImpl({ const hasInputFocusedRef = useRef(false) const renameInputRef = useRef(null) const searchInputRef = useRef(null) + const preserveSearchFocus = (event: React.PointerEvent) => { + if (searchInputRef.current === document.activeElement) event.preventDefault() + } const workspaceListRef = useRef(null) /** * Held in state as well as the ref: the list lives in the menu's portal, which @@ -263,13 +271,10 @@ function WorkspaceHeaderImpl({ setIsKeyboardNav(false) }, [isWorkspaceMenuOpen]) - const [isMounted, setIsMounted] = useState(false) - useEffect(() => { - setIsMounted(true) - }, []) - const { navigateToSettings } = useSettingsNavigation() const queryClient = useQueryClient() + const hostContext = useWorkspaceHostContext() + const organizationHref = getWorkspaceOrganizationHref(hostContext) const activeWorkspaceFull = workspaces.find((w) => w.id === workspaceId) || null const isWorkspaceReady = !isWorkspacesLoading && activeWorkspaceFull !== null @@ -443,7 +448,7 @@ function WorkspaceHeaderImpl({ return (
- {isMounted && isCollapsed ? ( + {isCollapsed ? ( } /> - ) : isMounted && isWorkspaceReady ? ( + ) : isWorkspaceReady ? ( { @@ -525,6 +530,17 @@ function WorkspaceHeaderImpl({ screen with nothing able to scroll to them. */ className='flex max-h-[var(--radix-dropdown-menu-content-available-height,400px)] w-64 max-w-[calc(100vw-24px)] flex-col overflow-y-auto' > + {organizationHref && ( + <> + + + + Back to organization + + + + + )} {isWorkspacesLoading ? (
Loading workspaces... @@ -534,6 +550,7 @@ function WorkspaceHeaderImpl({ {showSearch && ( {editingWorkspaceId === workspace.id ? ( ) : ( -
event.preventDefault()} + onPointerMove={preserveSearchFocus} + onPointerLeave={preserveSearchFocus} + actionIndicator={ + pinnedWorkspaceIds.has(workspace.id) ? ( + + ) : undefined + } + action={ + { + isContextMenuOpeningRef.current = true + }} + onClick={(event) => { + const rect = event.currentTarget.getBoundingClientRect() + openContextMenuAt(workspace, rect.right, rect.top) + }} + > + + + } onClick={(e) => { if (e.metaKey || e.ctrlKey) { window.open(`/workspace/${workspace.id}`, '_blank') @@ -693,81 +733,39 @@ function WorkspaceHeaderImpl({ logoUrl={workspace.logoUrl} alt={workspace.name || 'Workspace logo'} /> - - {/* Pin and options share one fixed slot, as the chat rows do: - the trailing width never changes, so pinning cannot re-truncate - the name under the user's cursor. */} -
- {pinnedWorkspaceIds.has(workspace.id) && ( - - )} - -
-
+ + )}
) })}
- + -
+
- { - e.stopPropagation() - if (!canCreateWorkspace) return + { + if (!canCreateWorkspace) { + event.preventDefault() + return + } setIsWorkspaceMenuOpen(false) setIsCreateModalOpen(true) }} - disabled={isCreatingWorkspace} - aria-disabled={!canCreateWorkspace || undefined} - fullWidth - className={cn( - 'select-none', - !canCreateWorkspace && - 'cursor-not-allowed opacity-60 hover-hover:bg-transparent' - )} + disabled={isCreatingWorkspace || !canCreateWorkspace} > + New workspace - + {userPermissions.canAdmin && ( - { + { setIsWorkspaceMenuOpen(false) if (isInvitationsDisabled) { if (billingEnabled) navigateToSettings({ section: 'billing' }) @@ -775,11 +773,10 @@ function WorkspaceHeaderImpl({ } setIsInviteModalOpen(true) }} - fullWidth - className='select-none' > + Invite teammates - + )} + id === 'teammates' || + id === 'recently-deleted' || + (id === 'billing' && canViewWorkspaceBillingSettings(hostContext, profile?.id)) + ) + .map(({ id, label, icon }) => ({ + label, + icon, + href: getSettingsHref({ section: id }), + onNavigate: () => handleOpenSettings(id), + })) + + const organizationHref = getWorkspaceOrganizationHref(hostContext) + if (organizationHref) { + profileNavigationLinks.push({ + label: 'Organization', + icon: Building, + href: organizationHref, + onNavigate: () => router.push(organizationHref), + }) + } + const { data: fetchedChats = EMPTY_CHATS, isLoading: chatsLoading } = useMothershipChats( workspaceId, { enabled: chatEnabled } @@ -1764,12 +1797,12 @@ export const Sidebar = memo(function Sidebar() { ) : null} getSettingsHref({ section })} - onOpenSettings={handleOpenSettings} + accountSettingsHref={getSettingsHref({ section: 'general' })} + onOpenAccountSettings={() => handleOpenSettings('general')} + navigationLinks={profileNavigationLinks} onOpenDocs={handleOpenDocs} onJoinSlack={handleOpenSlackCommunity} onContactSupport={handleOpenHelpFromMenu} diff --git a/apps/sim/lib/auth/sign-out.test.ts b/apps/sim/lib/auth/sign-out.test.ts new file mode 100644 index 00000000000..e580005abe8 --- /dev/null +++ b/apps/sim/lib/auth/sign-out.test.ts @@ -0,0 +1,46 @@ +/** + * @vitest-environment node + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ signOut: vi.fn(), clearUserData: vi.fn() })) +vi.mock('@/lib/auth/auth-client', () => ({ signOut: mocks.signOut })) +vi.mock('@/stores', () => ({ clearUserData: mocks.clearUserData })) + +import { signOutAndRedirect } from '@/lib/auth/sign-out' + +const navigate = vi.fn() +const assign = vi.fn() + +beforeEach(() => { + vi.clearAllMocks() + mocks.signOut.mockResolvedValue(undefined) + mocks.clearUserData.mockResolvedValue(true) + vi.stubGlobal('window', { location: { assign } }) +}) +afterEach(() => vi.unstubAllGlobals()) + +describe('signOutAndRedirect', () => { + it('ends the session and clears user state before navigating in-app', async () => { + await signOutAndRedirect(navigate) + expect(mocks.signOut).toHaveBeenCalledOnce() + expect(mocks.clearUserData).toHaveBeenCalledOnce() + expect(navigate).toHaveBeenCalledWith('/login?fromLogout=true') + expect(assign).not.toHaveBeenCalled() + }) + + it('reloads when in-memory state could not be cleared', async () => { + mocks.clearUserData.mockResolvedValue(false) + await signOutAndRedirect(navigate) + expect(navigate).not.toHaveBeenCalled() + expect(assign).toHaveBeenCalledWith('/login?fromLogout=true') + }) + + it('reloads on sign-out failure and still attempts to clear user state', async () => { + mocks.signOut.mockRejectedValue(new Error('offline')) + await signOutAndRedirect(navigate) + expect(mocks.clearUserData).toHaveBeenCalledOnce() + expect(navigate).not.toHaveBeenCalled() + expect(assign).toHaveBeenCalledWith('/login?fromLogout=true') + }) +}) diff --git a/apps/sim/lib/auth/sign-out.ts b/apps/sim/lib/auth/sign-out.ts new file mode 100644 index 00000000000..4f0e4927446 --- /dev/null +++ b/apps/sim/lib/auth/sign-out.ts @@ -0,0 +1,21 @@ +import { createLogger } from '@sim/logger' +import { signOut } from '@/lib/auth/auth-client' +import { clearUserData } from '@/stores' + +const logger = createLogger('SignOut') + +/** Reloads the page if any in-memory user state could not be cleared. */ +export async function signOutAndRedirect(navigate: (href: string) => void): Promise { + const logoutUrl = '/login?fromLogout=true' + let canNavigateInApp = false + + try { + const [, inMemoryResetSucceeded] = await Promise.all([signOut(), clearUserData()]) + canNavigateInApp = inMemoryResetSucceeded + } catch (error) { + logger.error('Error signing out:', { error }) + } + + if (canNavigateInApp) navigate(logoutUrl) + else window.location.assign(logoutUrl) +} diff --git a/apps/sim/lib/workspaces/organization-navigation.ts b/apps/sim/lib/workspaces/organization-navigation.ts new file mode 100644 index 00000000000..758bf66f2d1 --- /dev/null +++ b/apps/sim/lib/workspaces/organization-navigation.ts @@ -0,0 +1,11 @@ +import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces' +import { organizationRoutes } from '@/lib/navigation/paths' + +/** Returns the workspace's organization destination only when the viewer can enter it. */ +export function getWorkspaceOrganizationHref(hostContext: WorkspaceHostContext): string | null { + return hostContext.hostOrganizationId && + hostContext.viewer.isHostOrganizationMember && + hostContext.features?.organizationSearch + ? organizationRoutes(hostContext.hostOrganizationId).root + : null +} diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx index 87d6295f58d..8417f9e6208 100644 --- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx @@ -22,9 +22,10 @@ import * as React from 'react' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { cva, type VariantProps } from 'class-variance-authority' import { Check, ChevronRight, Circle, Search } from '../../icons' import { cn } from '../../lib/cn' -import { chipContentGap, chipFieldSurfaceClass } from '../chip/chip-chrome' +import { chipContentGap, chipFieldSurfaceClass, chipGeometryClass } from '../chip/chip-chrome' import { InsideModalContext } from '../modal/modal' import { OverflowText, type OverflowTextProps } from '../overflow-text/overflow-text' @@ -300,64 +301,87 @@ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName */ export const dropdownMenuRowClass = `relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-pointer select-none items-center ${chipContentGap} ${MENU_ROW_RADIUS_CLASS} px-2 text-[var(--text-body)] text-small outline-hidden ${MENU_ROW_TRANSITION_CLASS} data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ${MENU_ROW_SINGLE_LINE_CLASS} [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]` +/** Large rows match the sidebar's chip geometry without changing menu behavior. */ +export const dropdownMenuItemVariants = cva(dropdownMenuRowClass, { + variants: { + size: { default: '', lg: chipGeometryClass }, + }, + defaultVariants: { size: 'default' }, +}) + const DropdownMenuItem = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - /** - * Renders the row as selected — the current route, the checked value, the row - * whose own menu is open. Selected is a state the row *holds*, so it keeps - * `--surface-active` through hover rather than dimming to the hover fill. - * - * Not for a pointer/keyboard cursor: that is the row highlight, which the row - * already paints on its own. A menu that marks its cursor row `active` puts two - * selections on screen. - */ - active?: boolean - /** - * Optional inline action rendered on the right edge of the item — e.g. a - * "more" icon button. Reveals on hover/focus of the row, and the row stays - * highlighted while the cursor is over the action. - */ - action?: React.ReactNode - } ->(({ className, inset, active, action, asChild, children, ...props }, ref) => { - const content = asChild ? children : withOverflowLabel(children) - const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS - if (action) { - return ( -
- - {content} - -
- {action} + React.ComponentPropsWithoutRef & + VariantProps & { + inset?: boolean + /** + * Renders the row as selected — the current route, the checked value, the row + * whose own menu is open. Selected is a state the row *holds*, so it keeps + * `--surface-active` through hover rather than dimming to the hover fill. + * + * Not for a pointer/keyboard cursor: that is the row highlight, which the row + * already paints on its own. A menu that marks its cursor row `active` puts two + * selections on screen. + */ + active?: boolean + /** + * Optional inline action rendered on the right edge of the item — e.g. a + * "more" icon button. Reveals on hover/focus of the row, and the row stays + * highlighted while the cursor is over the action. + */ + action?: React.ReactNode + /** Idle indicator sharing the action slot so the label never shifts. */ + actionIndicator?: React.ReactNode + } +>( + ( + { className, size, inset, active, action, actionIndicator, asChild, children, ...props }, + ref + ) => { + const content = asChild ? children : withOverflowLabel(children) + const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS + if (action) { + return ( +
+ + {content} + +
+ {actionIndicator && ( +
+ {actionIndicator} +
+ )} +
+ {action} +
+
-
+ ) + } + return ( + + {content} + ) } - return ( - - {content} - - ) -}) +) DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName /** From 9294beb326ac653477020fd0f544d6086728973c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 11 Sep 2026 17:35:23 -0700 Subject: [PATCH 06/15] fix(search): use compact candidates for filtered vector retrieval (#7794) --- .../server/knowledge/workspace-search.test.ts | 23 + .../server/knowledge/workspace-search.ts | 2 +- .../search-latency.integration.ts | 73 +- .../lib/knowledge/application/search.test.ts | 32 +- apps/sim/lib/knowledge/application/search.ts | 3 - apps/sim/lib/knowledge/search/diagnostics.ts | 3 + apps/sim/lib/knowledge/search/queries.test.ts | 19 +- apps/sim/lib/knowledge/search/queries.ts | 97 +- apps/sim/lib/knowledge/vector-columns.test.ts | 15 + apps/sim/lib/knowledge/vector-columns.ts | 9 + ...embedding-width-migration.postgres.test.ts | 36 + .../db/migrations/0342_clean_weapon_omega.sql | 19 + .../db/migrations/meta/0342_snapshot.json | 26392 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 17 + 15 files changed, 26700 insertions(+), 47 deletions(-) create mode 100644 packages/db/migrations/0342_clean_weapon_omega.sql create mode 100644 packages/db/migrations/meta/0342_snapshot.json diff --git a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts index b83a791fc06..f14efc854e8 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.test.ts @@ -84,6 +84,29 @@ describe('Assistant retrieval tools', () => { next: null, }) }) + it('returns empty incomplete retrieval as a recoverable search outcome and logs coverage', async () => { + mocks.search.mockResolvedValue({ + retrieval: { status: 'partial', timedOutLegs: ['vector', 'keyword'] }, + knowledgeBases: [{ id: 'index', name: 'Enterprise Search' }], + results: [], + }) + + const result = await searchWorkspaceServerTool.execute({ query: 'canaries' }, context) + + expect(result).toMatchObject({ + success: true, + message: expect.stringContaining('cannot establish absence or completeness'), + data: { + retrieval: { status: 'partial', timedOutLegs: ['vector', 'keyword'] }, + results: [], + }, + }) + expect(result).not.toHaveProperty('error') + expect(mocks.info).toHaveBeenCalledWith( + 'Knowledge search completed', + expect.objectContaining({ passageBytes: 0, originalPassageBytes: 0, outcome: 'success' }) + ) + }) it('pins organization and private chat while reusing the canonical search index and citations', async () => { const orgContext = { ...context, diff --git a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts index 8b014c4d17c..7d24d5969da 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/workspace-search.ts @@ -93,7 +93,7 @@ export const searchWorkspaceServerTool: BaseServerTool = { const names = new Map(result.knowledgeBases.map((base) => [base.id, base.name])) const output = { success: true, - message: `${result.retrieval.status === 'partial' ? 'Partial search: a retrieval branch reached its deadline. These results cannot establish absence or completeness. ' : ''}Found ${result.results.length} passage previews. Read a document at its chunkIndex for more context. ${CITATION_INSTRUCTION}`, + message: `${result.retrieval.status === 'partial' ? 'Search coverage is incomplete. Continue with a more specific query or source filter; these results cannot establish absence or completeness. ' : ''}Found ${result.results.length} passage previews. Read a document at its chunkIndex for more context. ${CITATION_INSTRUCTION}`, data: { query: safeQuery, retrieval: result.retrieval, diff --git a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts index b1792cee86d..7f62c21dae8 100644 --- a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts @@ -83,7 +83,11 @@ const reused = reuseFile ? readFixtureReport(reuseFile) : undefined const ids = reused?.fixture ?? createKnowledgeAclFixtureIds() const unrelated = reused?.unrelatedFixture ?? createKnowledgeAclFixtureIds() const organizationChatId = generateId() -const queryVector = Array.from({ length: dimensions }, (_, index) => (index === 0 ? 1 : 0)) +const queryVector = Array.from({ length: dimensions }, (_, index) => + Math.sin((index + 1) * 12.9898) +) +const queryMagnitude = Math.hypot(...queryVector) +for (let index = 0; index < queryVector.length; index++) queryVector[index] /= queryMagnitude const captured: CapturedQuery[] = [] const report: Record = { fixture: ids, @@ -133,6 +137,7 @@ const explainSchema = z.array(z.object({ Plan: explainNodeSchema }).passthrough( function usesVectorIndex(node: ExplainNode): boolean { return ( + node['Index Name'] === 'embedding_binary_hnsw_idx' || node['Index Name'] === 'embedding_vector_hnsw_idx' || (node.Plans?.some(usesVectorIndex) ?? false) ) @@ -237,14 +242,21 @@ async function sample(label: string, run: () => ReturnType) { const plan = await db.$client.begin(async (tx) => { await tx.unsafe("SET LOCAL hnsw.iterative_scan = 'relaxed_order'") await tx.unsafe('SET LOCAL hnsw.max_scan_tuples = 20000') + if (query.query.includes('binary_quantize')) { + await tx.unsafe('SET LOCAL hnsw.max_scan_tuples = 100000') + await tx.unsafe('SET LOCAL hnsw.ef_search = 200') + await tx.unsafe('SET LOCAL hnsw.scan_mem_multiplier = 4') + } return tx.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.query}`, query.parameters) }) plans.push({ kind: query.query.includes('keyword_rank') ? 'keyword' - : query.query.includes('order by') + : query.query.includes('binary_quantize') ? 'vector' - : 'probe', + : query.query.includes('order by') + ? 'rerank' + : 'probe', query: query.query, parameters: query.parameters, plan: explainSchema.parse(plan[0]['QUERY PLAN']), @@ -378,8 +390,8 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu CASE WHEN n % 8 = 0 THEN 'Orion deployment reference. ' ELSE 'Engineering operations reference. ' END || (SELECT string_agg(md5(n::text || ':' || paragraph::text), ' ') FROM generate_series(1, 90) paragraph), 3000, 750, 0, 3000, - l2_normalize(ARRAY(SELECT (CASE WHEN coordinate = n % 32 + 1 THEN 1 ELSE 0 END + - 0.025 * sin(n::double precision * coordinate * 12.9898 + coordinate * 78.233))::real + l2_normalize(ARRAY(SELECT (sin(coordinate * (n % 32 + 1) * 12.9898) + + 0.25 * sin(n::double precision * coordinate * 12.9898 + coordinate * 78.233))::real FROM generate_series(1, ${dimensions}) coordinate)::vector(1536)) FROM generate_series(${first}::int, ${last}::int) n`) }) @@ -494,7 +506,13 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu } ) if (delayedLegs === 'both') { - expect(result).toMatchObject({ success: false, retryable: true }) + expect(result).toMatchObject({ + success: true, + data: { + retrieval: { status: 'partial', timedOutLegs: ['vector', 'keyword'] }, + results: [], + }, + }) return } expect(result).toMatchObject({ @@ -526,6 +544,18 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu const vectorPlans = plans.filter((plan) => plan.kind === 'vector') expect(vectorPlans).toHaveLength(1) expect(usesVectorIndex(vectorPlans[0].plan[0].Plan)).toBe(true) + expect(plans.some((plan) => plan.kind === 'rerank')).toBe(true) + const rerank = plans.find((plan) => plan.kind === 'rerank')! + const actual = await db.$client.unsafe(rerank.query, rerank.parameters).values() + const expected = await db.execute<{ id: string }>(sql`SELECT id FROM embedding + WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND enabled + ORDER BY (embedding <=> ${JSON.stringify(queryVector)}::vector) + 0, id + LIMIT ${actual.length}`) + const expectedIds = new Set(expected.map(({ id }) => id)) + const recall = actual.filter(([id]) => expectedIds.has(id)).length / expected.length + expect(recall).toBeGreaterThanOrEqual(0.95) + report[`recall.${iteration}`] = { neighbors: expected.length, recall } + saveReport() } expect(embeddingCalls - before).toBe(2) }, 180_000) @@ -578,7 +608,7 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu expect(probe).toHaveLength(1) expect(probe[0].query).not.toContain('<=>') expect(probe[0].plan[0].Plan['Actual Rows']).toBe(12) - const vector = plans.filter((plan) => plan.kind === 'vector') + const vector = plans.filter((plan) => plan.kind === 'rerank') expect(vector).toHaveLength(1) expect(vector[0].query).toContain('"embedding"."id" in') } finally { @@ -672,7 +702,7 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu }, 180_000) /** Opt in with local Sim and Go URLs; uses the real configured provider, billing adapter, and async resume protocol. */ it.skipIf(!process.env.KNOWLEDGE_SEARCH_ASSISTANT_URL)( - 'answers through local Go Assistant with progressive reads and citations', + 'recovers quietly from incomplete search through local Go Assistant, then reads and cites evidence', async () => { const assistantUrl = new URL(process.env.KNOWLEDGE_SEARCH_ASSISTANT_URL!) const simUrl = new URL(process.env.KNOWLEDGE_SEARCH_SIM_URL!) @@ -708,6 +738,18 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu bytes: number }> = [] let answer = '' + let incompleteSearch = true + const query = SearchBudget.prototype.query + const delayed = vi.spyOn(SearchBudget.prototype, 'query').mockImplementation(function ( + this: SearchBudget, + stage: SearchStage, + run: (executor: SearchExecutor) => PromiseLike + ): Promise { + return query.call(this, stage, async (tx) => { + if (incompleteSearch) await tx.execute(sql`SELECT pg_sleep(9)`) + return run(tx) + }) as Promise + }) const started = performance.now() try { await db @@ -808,9 +850,20 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu bytes: Buffer.byteLength(JSON.stringify(result)), }) const { success } = z.object({ success: z.boolean() }).parse(result) + expect(success).toBe(true) + if (incompleteSearch) { + expect(call.toolName).toBe('search_workspace') + expect(result).toMatchObject({ + data: { + retrieval: { status: 'partial', timedOutLegs: ['vector', 'keyword'] }, + results: [], + }, + }) + } return { callId, name: call.toolName, success, data: result } }) ) + incompleteSearch = false path = '/api/tools/resume' body = { checkpointId: checkpoint.checkpointId, @@ -828,10 +881,12 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu expect(answer).toContain('SILVER COMET') expect(answer).toContain('K7M2-84') expect(answer).toContain('') + expect(answer).not.toMatch(/timed?\s*out|timeout|internal retr(?:y|ies)/i) expect(calls.some((call) => call.name === 'read_document')).toBe(true) - expect(calls.some((call) => call.name === 'search_workspace')).toBe(true) + expect(calls.filter((call) => call.name === 'search_workspace').length).toBeGreaterThan(1) expect(calls.every((call) => call.bytes < 40000)).toBe(true) } finally { + delayed.mockRestore() await db.update(embedding).set(original).where(eq(embedding.id, chunkId)) } }, diff --git a/apps/sim/lib/knowledge/application/search.test.ts b/apps/sim/lib/knowledge/application/search.test.ts index 9adf6797f47..d34cc59cfaf 100644 --- a/apps/sim/lib/knowledge/application/search.test.ts +++ b/apps/sim/lib/knowledge/application/search.test.ts @@ -19,6 +19,7 @@ const mocks = vi.hoisted(() => ({ checkActorUsage: vi.fn(), generateEmbedding: vi.fn(), executeSearch: vi.fn(), + retrieval: vi.fn(), getDocumentMetadata: vi.fn(), getTagDefinitions: vi.fn(), getTagDefinitionsBatch: vi.fn(), @@ -91,7 +92,7 @@ vi.mock('@/lib/knowledge/search/queries', () => ({ generateSearchEmbedding: mocks.generateEmbedding, retrieveKnowledgeSearch: async (...args: unknown[]) => ({ rows: await mocks.executeSearch(...args), - retrieval: { status: 'complete', timedOutLegs: [] }, + retrieval: mocks.retrieval(), }), getDocumentMetadataByIds: mocks.getDocumentMetadata, })) @@ -129,6 +130,7 @@ const knowledgeBase = { describe('knowledge search application use case', () => { beforeEach(() => { + mocks.retrieval.mockReturnValue({ status: 'complete', timedOutLegs: [] }) vi.clearAllMocks() mocks.rerank.mockReset() resetDbChainMock() @@ -204,6 +206,34 @@ describe('knowledge search application use case', () => { expect(result.totalResults).toBe(0) }) + it.each([false, true])( + 'requires explicit partial-result support for empty incomplete searches (allowPartialResults=%s)', + async (allowPartialResults) => { + mocks.retrieval.mockReturnValue({ status: 'partial', timedOutLegs: ['vector', 'keyword'] }) + mocks.executeSearch.mockResolvedValue([]) + const result = searchKnowledge.execute({ + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + input: { + workspaceId: 'workspace-1', + knowledgeBaseIds: ['knowledge-1'], + query: 'canaries', + topK: 20, + allowPartialResults, + }, + }) + + if (!allowPartialResults) { + await expect(result).rejects.toThrow('retrieval deadline') + return + } + await expect(result).resolves.toMatchObject({ + results: [], + totalResults: 0, + retrieval: { status: 'partial', timedOutLegs: ['vector', 'keyword'] }, + }) + } + ) + describe.each(['workspace', 'organization'] as const)('%s ranking policy', (scope) => { beforeEach(() => { if (scope === 'organization') { diff --git a/apps/sim/lib/knowledge/application/search.ts b/apps/sim/lib/knowledge/application/search.ts index b67f2fef3fc..8d2e1dc2057 100644 --- a/apps/sim/lib/knowledge/application/search.ts +++ b/apps/sim/lib/knowledge/application/search.ts @@ -709,9 +709,6 @@ const searchKnowledgeUseCase = defineAuthorizedKnowledgeUseCase({ } } annotateSearchDiagnostics({ resultCount: results.length }) - if (retrieved.retrieval.status === 'partial' && results.length === 0) { - throw new SearchDeadlineError() - } const cost = baseCost ? { input: baseCost.input, diff --git a/apps/sim/lib/knowledge/search/diagnostics.ts b/apps/sim/lib/knowledge/search/diagnostics.ts index bc8e5c4e921..f4c506ca629 100644 --- a/apps/sim/lib/knowledge/search/diagnostics.ts +++ b/apps/sim/lib/knowledge/search/diagnostics.ts @@ -47,6 +47,7 @@ export type SearchStage = | 'vector.settings' | 'vector.probe' | 'vector.ann' + | 'vector.rerank' | 'vector.exact' /** Fixed, content-free fields. Never pass queries, filters, document identities, SQL, or errors. */ @@ -68,6 +69,8 @@ export interface SearchDiagnosticMetadata { searchMode?: 'hybrid' | 'vector' boostRecency?: boolean embeddingDimensions?: number + vectorRanking?: 'exact' | 'binary-rerank' + vectorCandidateCount?: number resultCount?: number /** Tool output before the executor's final egress projection; counts only, never content. */ toolResultBytes?: number diff --git a/apps/sim/lib/knowledge/search/queries.test.ts b/apps/sim/lib/knowledge/search/queries.test.ts index a1b9728d06e..168c64609ed 100644 --- a/apps/sim/lib/knowledge/search/queries.test.ts +++ b/apps/sim/lib/knowledge/search/queries.test.ts @@ -528,6 +528,7 @@ describe('live repository authorization follows ranked candidates', () => { schemaMock.embedding, Array.from({ length: 200 }, (_, index) => candidate(`probe-${index}`, 'allowed-source')) ) + queueTableRows(schemaMock.embedding, [{ id: 'far' }, { id: 'near' }]) queueTableRows(schemaMock.embedding, [ { ...candidate('far', 'allowed-source'), distance: 0.3 }, { ...candidate('near', 'allowed-source'), distance: 0.1 }, @@ -544,13 +545,17 @@ describe('live repository authorization follows ranked candidates', () => { }) expect(rows.map((row) => row.id)).toEqual(['near']) expect(dbChainMockFns.orderBy.mock.calls[0]).toHaveLength(1) - expect(Object.keys(dbChainMockFns.select.mock.calls[1][0])).toEqual(['id', 'distance']) + expect(Object.keys(dbChainMockFns.select.mock.calls[1][0])).toEqual(['id']) + expect(render(dbChainMockFns.orderBy.mock.calls[0][0]).sql).toContain('binary_quantize') + expect(JSON.stringify(dbChainMockFns.orderBy.mock.calls[1][0])).toContain('<=>') + expect(dbChainMockFns.limit.mock.calls[1]).toEqual([4000]) + expect(JSON.stringify(dbChainMockFns.where.mock.calls[1][0])).not.toContain('<=>') expect(dbChainMockFns.limit.mock.invocationCallOrder[1]).toBeLessThan( dbChainMockFns.select.mock.invocationCallOrder[2] ) expect(JSON.stringify(dbChainMockFns.where.mock.calls[1][0])).toContain('OFFSET 0') expect(getForConnectors).toHaveBeenCalledExactlyOnceWith(['allowed-source'], undefined) - expect(JSON.stringify(dbChainMockFns.where.mock.calls[2][0])).toContain('github_read_grant') + expect(JSON.stringify(dbChainMockFns.where.mock.calls[3][0])).toContain('github_read_grant') }) it('finishes empty scopes after the bounded probe without scanning HNSW or calling providers', async () => { @@ -592,6 +597,7 @@ describe('live repository authorization follows ranked candidates', () => { schemaMock.embedding, Array.from({ length: 200 }, (_, index) => candidate(`probe-${index}`, 'allowed-source')) ) + queueTableRows(schemaMock.embedding, [{ id: 'partial' }]) queueTableRows(schemaMock.embedding, [candidate('partial', 'allowed-source')]) queueTableRows(schemaMock.embedding, [candidate('selected', 'allowed-source')]) queueTableRows(schemaMock.embedding, [ @@ -614,6 +620,10 @@ describe('live repository authorization follows ranked candidates', () => { candidate(`approximate-${index}`, 'allowed-source') ) queueTableRows(schemaMock.embedding, probe) + queueTableRows( + schemaMock.embedding, + approximate.map(({ id }) => ({ id })) + ) queueTableRows(schemaMock.embedding, approximate) queueTableRows(schemaMock.embedding, []) queueTableRows(schemaMock.embedding, probe) @@ -628,7 +638,7 @@ describe('live repository authorization follows ranked candidates', () => { const rows = await handleVectorOnlySearch({ ...params, structuredFilters: undefined }) expect(rows.map((row) => row.id)).toEqual(['selected']) - expect(dbChainMockFns.offset.mock.calls).toEqual([[0], [20], [0], [20]]) + expect(dbChainMockFns.offset.mock.calls).toEqual([[0], [0], [20]]) expect(getForConnectors).toHaveBeenCalledTimes(2) expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(2) }) @@ -638,6 +648,7 @@ describe('live repository authorization follows ranked candidates', () => { candidate(`probe-${index}`, 'allowed-source') ) queueTableRows(schemaMock.embedding, probe) + queueTableRows(schemaMock.embedding, [{ id: 'far' }]) queueTableRows(schemaMock.embedding, [ { ...candidate('far', 'allowed-source'), distance: 0.7 }, ...Array.from({ length: 19 }, (_, index) => candidate(`hidden-${index}`, 'allowed-source')), @@ -662,7 +673,7 @@ describe('live repository authorization follows ranked candidates', () => { }) expect(rows.map((row) => row.id)).toEqual(['nearer', 'near']) - expect(dbChainMockFns.offset.mock.calls).toEqual([[0], [20], [0]]) + expect(dbChainMockFns.offset.mock.calls).toEqual([[0], [0]]) expect( hasMockCondition( dbChainMockFns.where.mock.calls.at(-1)![0], diff --git a/apps/sim/lib/knowledge/search/queries.ts b/apps/sim/lib/knowledge/search/queries.ts index 91ff5d12957..2e349830dea 100644 --- a/apps/sim/lib/knowledge/search/queries.ts +++ b/apps/sim/lib/knowledge/search/queries.ts @@ -17,7 +17,11 @@ import { SearchDeadlineError, type SearchExecutor, } from '@/lib/knowledge/search/budget' -import { measureSearchStage, recordSearchStageDuration } from '@/lib/knowledge/search/diagnostics' +import { + annotateSearchDiagnostics, + measureSearchStage, + recordSearchStageDuration, +} from '@/lib/knowledge/search/diagnostics' import { workspaceSearchFilterConditions } from '@/lib/knowledge/search/filter-conditions' import type { WorkspaceSearchFilters } from '@/lib/knowledge/search/filters' import { applyRecencyBoost, RRF_K } from '@/lib/knowledge/search/recency' @@ -27,7 +31,7 @@ import { uncompilableTagFilterError, } from '@/lib/knowledge/tags/utils' import type { StructuredFilter } from '@/lib/knowledge/types' -import { embeddingDistance } from '@/lib/knowledge/vector-columns' +import { embeddingCandidateDistance, embeddingDistance } from '@/lib/knowledge/vector-columns' const logger = createLogger('KnowledgeSearchQueries') @@ -35,6 +39,14 @@ const logger = createLogger('KnowledgeSearchQueries') const UNDEFINED_OBJECT_SQLSTATE = '42704' /** Tuples a relaxed-order scan may visit before giving up on filling the limit. */ const HNSW_MAX_SCAN_TUPLES = '20000' +/** Compact graphs can visit a wider frontier without retaining full vectors for each neighbor. */ +const BINARY_HNSW_MAX_SCAN_TUPLES = '100000' +const BINARY_HNSW_EF_SEARCH = '200' +const BINARY_HNSW_SCAN_MEM_MULTIPLIER = '4' +/** Bounded cosine reranking pool, sized for recall under permission filtering and sign quantization. */ +const MIN_VECTOR_RERANK_CANDIDATES = 4000 +const MAX_VECTOR_RERANK_CANDIDATES = 8000 +const VECTOR_RERANK_OVERSAMPLING = 40 /** How long to stop trying the iterative-scan settings after the server rejected them. */ const HNSW_SETTINGS_UNSUPPORTED_RETRY_MS = 10 * 60 * 1000 @@ -49,7 +61,8 @@ let hnswSettingsUnsupportedUntil = 0 */ async function withVectorScanSettings( run: (executor: SearchExecutor) => Promise, - budget?: SearchBudget + budget?: SearchBudget, + ranking: 'cosine' | 'binary' = 'cosine' ): Promise { const untuned = () => runSearchQuery(budget, 'vector.ann', run) if (Date.now() < hnswSettingsUnsupportedUntil) return untuned() @@ -62,7 +75,9 @@ async function withVectorScanSettings( applyingSettings = true await measureSearchStage('vector.settings', () => tx.execute( - sql`SELECT set_config('hnsw.iterative_scan', 'relaxed_order', true), set_config('hnsw.max_scan_tuples', ${HNSW_MAX_SCAN_TUPLES}, true)` + ranking === 'binary' + ? sql`SELECT set_config('hnsw.iterative_scan', 'relaxed_order', true), set_config('hnsw.max_scan_tuples', ${BINARY_HNSW_MAX_SCAN_TUPLES}, true), set_config('hnsw.ef_search', ${BINARY_HNSW_EF_SEARCH}, true), set_config('hnsw.scan_mem_multiplier', ${BINARY_HNSW_SCAN_MEM_MULTIPLIER}, true)` + : sql`SELECT set_config('hnsw.iterative_scan', 'relaxed_order', true), set_config('hnsw.max_scan_tuples', ${HNSW_MAX_SCAN_TUPLES}, true)` ) ) applyingSettings = false @@ -778,11 +793,12 @@ export async function handleVectorOnlySearch(params: SearchParams): Promise { const conditions = [inArray(embedding.knowledgeBaseId, params.knowledgeBaseIds), ...filters] + const queryVector = params.queryVector! + const candidateDistance = embeddingCandidateDistance(queryVector.dimensions, queryVector.vector) + const candidateLimit = Math.min( + MAX_VECTOR_RERANK_CANDIDATES, + Math.max(MIN_VECTOR_RERANK_CANDIDATES, params.topK * VECTOR_RERANK_OVERSAMPLING) + ) let useExactRanking = false const rows = await selectAuthorizedSearchResults({ leg: 'vector', @@ -815,6 +837,7 @@ async function selectLiveVectorResults( const exactPage = async (candidateIds?: string[]) => { const exactOffset = useExactRanking ? offset : 0 useExactRanking = true + annotateSearchDiagnostics({ vectorRanking: 'exact' }) const candidates = await runSearchQuery(params.budget, 'vector.exact', (executor) => executor .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: distance.as('distance') }) @@ -850,35 +873,53 @@ async function selectLiveVectorResults( return exactPage(probe.map((candidate) => candidate.id)) } if (useExactRanking) return exactPage() - const ann = async (executor: SearchExecutor) => { - const ranked = executor - .select({ id: embedding.id, distance: distance.as('distance') }) - .from(embedding) - .where( - and( - ...conditions, - sql`EXISTS ( + const identities = await withVectorScanSettings( + (executor) => + executor + .select({ id: embedding.id }) + .from(embedding) + .where( + and( + inArray(embedding.knowledgeBaseId, params.knowledgeBaseIds), + sql`EXISTS ( SELECT 1 FROM ${document} WHERE ${and(eq(document.id, embedding.documentId), ...visibility)} OFFSET 0 )` + ) + ) + .orderBy(candidateDistance) + .limit(candidateLimit), + params.budget, + 'binary' + ) + annotateSearchDiagnostics({ + vectorRanking: 'binary-rerank', + vectorCandidateCount: identities.length, + }) + if (!identities.length) return exactPage() + const page = await runSearchQuery(params.budget, 'vector.rerank', (executor) => + executor + .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: distance.as('distance') }) + .from(embedding) + .innerJoin(document, eq(document.id, embedding.documentId)) + .where( + and( + inArray( + embedding.id, + identities.map(({ id }) => id) + ), + ...conditions, + ...visibility ) ) - .orderBy(distance) + .orderBy(sql`(${distance}) + 0`, embedding.id) .limit(limit) .offset(offset) - .as('ranked_search_candidates') - return executor - .select({ ...SEARCH_READ_CANDIDATE_FIELDS, distance: ranked.distance }) - .from(ranked) - .innerJoin(embedding, eq(embedding.id, ranked.id)) - .innerJoin(document, eq(document.id, embedding.documentId)) - .orderBy(ranked.distance, ranked.id) - } - const page = await withVectorScanSettings(ann, params.budget) + ) if (page.length < limit) return exactPage() return { - candidates: page.sort((a, b) => a.distance - b.distance), + candidates: page, nextOffset: offset + page.length, } }, @@ -893,8 +934,7 @@ async function selectLiveVectorResults( params.budget ), }) - /** Relaxed HNSW scans can return adjacent pages out of distance order. */ - return rows.sort((a, b) => a.distance - b.distance) + return rows } /** @@ -1261,7 +1301,6 @@ export async function retrieveKnowledgeSearch( const timedOutLegs = Object.values(budgets) .filter((budget) => budget.timedOut) .map((budget) => budget.leg) - if (timedOutLegs.length && !rows.length) throw new SearchDeadlineError() return { rows: boostRecency ? applyRecencyBoost(rows) : rows, retrieval: { status: timedOutLegs.length ? 'partial' : 'complete', timedOutLegs }, diff --git a/apps/sim/lib/knowledge/vector-columns.test.ts b/apps/sim/lib/knowledge/vector-columns.test.ts index 68898ad4c0a..3d5f244a045 100644 --- a/apps/sim/lib/knowledge/vector-columns.test.ts +++ b/apps/sim/lib/knowledge/vector-columns.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest' import { KB_EMBEDDING_STORAGE_DIMENSIONS } from '@/lib/embeddings/catalog' import { + embeddingCandidateDistance, embeddingDistance, embeddingVectorColumn, embeddingVectorValues, @@ -20,6 +21,20 @@ describe('embeddingVectorColumn', () => { }) }) +describe('embeddingCandidateDistance', () => { + it('matches the compact expression index for every stored width', () => { + for (const width of KB_EMBEDDING_STORAGE_DIMENSIONS) { + const rendered = embeddingCandidateDistance(width, '[1,2]').toSQL() + expect(rendered.sql).toContain('binary_quantize(?)::bit(?) <~>') + expect(rendered.params[0]).toBe(embeddingVectorColumn(width)) + expect(rendered.params[1]).toEqual(rendered.params[3]) + expect(JSON.stringify(rendered.params[1])).toContain(String(width)) + expect(rendered.params[2]).toBe('[1,2]') + expect(rendered.sql).not.toContain('<=>') + } + }) +}) + describe('embeddingVectorValues', () => { const vector = [0.1, 0.2] diff --git a/apps/sim/lib/knowledge/vector-columns.ts b/apps/sim/lib/knowledge/vector-columns.ts index 3f6e084462c..057d168206e 100644 --- a/apps/sim/lib/knowledge/vector-columns.ts +++ b/apps/sim/lib/knowledge/vector-columns.ts @@ -69,3 +69,12 @@ export function embeddingDistance( } return sql`${embeddingVectorColumn(dimensions)} <=> ${queryVector}::vector` } + +/** Compact HNSW candidate ordering; final scores always use the stored vector's cosine distance. */ +export function embeddingCandidateDistance( + dimensions: KbEmbeddingDimensions, + queryVector: string +): SQL { + const width = sql.raw(String(dimensions)) + return sql`binary_quantize(${embeddingVectorColumn(dimensions)})::bit(${width}) <~> binary_quantize(${queryVector}::vector)::bit(${width})` +} diff --git a/packages/db/embedding-width-migration.postgres.test.ts b/packages/db/embedding-width-migration.postgres.test.ts index 738b00656d0..b4a3d9624f4 100644 --- a/packages/db/embedding-width-migration.postgres.test.ts +++ b/packages/db/embedding-width-migration.postgres.test.ts @@ -122,4 +122,40 @@ describe.runIf(Boolean(databaseUrl))('embedding width migration in PostgreSQL', sql`UPDATE embedding SET embedding_768 = ${vector(768)}::vector WHERE id = 'legacy'` ).rejects.toMatchObject({ code: '23514', constraint_name: 'embedding_width_check' }) }) + + it('rebuilds compact candidate indexes on replay and uses them at every stored width', async () => { + await applyMigration(correctiveMigration) + const migration = await readFile( + new URL('./migrations/0342_clean_weapon_omega.sql', import.meta.url), + 'utf8' + ) + for (const width of SUPPORTED_WIDTHS) { + await insertEmbedding(width, `width-${width}`) + const name = + width === 1536 ? 'embedding_binary_hnsw_idx' : `embedding_${width}_binary_hnsw_idx` + /** Seed interrupted builds in the isolated schema, never an index in the shared public schema. */ + await sql.unsafe(`CREATE INDEX "${name}" ON embedding (id)`) + } + await applyMigration(migration) + await applyMigration(migration) + await sql`SET enable_seqscan = off` + try { + for (const width of SUPPORTED_WIDTHS) { + const column = width === 1536 ? 'embedding' : `embedding_${width}` + const name = + width === 1536 ? 'embedding_binary_hnsw_idx' : `embedding_${width}_binary_hnsw_idx` + const plan = await sql.unsafe( + `EXPLAIN (FORMAT JSON) SELECT id FROM embedding + ORDER BY binary_quantize("${column}")::bit(${width}) <~> binary_quantize($1::vector)::bit(${width}) LIMIT 1`, + [vector(width)] + ) + expect(JSON.stringify(plan)).toContain(name) + } + const indexes = await sql`SELECT count(*)::int AS count FROM pg_index + WHERE indrelid = 'embedding'::regclass AND indisvalid` + expect(indexes[0].count).toBeGreaterThanOrEqual(10) + } finally { + await sql`RESET enable_seqscan` + } + }) }) diff --git a/packages/db/migrations/0342_clean_weapon_omega.sql b/packages/db/migrations/0342_clean_weapon_omega.sql new file mode 100644 index 00000000000..14de6fc5a21 --- /dev/null +++ b/packages/db/migrations/0342_clean_weapon_omega.sql @@ -0,0 +1,19 @@ +-- Additive indexes; existing cosine indexes remain available to deployed and unfiltered search. +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +-- migration-safe: replay removes only this new candidate index so an interrupted concurrent build cannot remain invalid. +DROP INDEX CONCURRENTLY IF EXISTS "embedding_binary_hnsw_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "embedding_binary_hnsw_idx" ON "embedding" USING hnsw ((binary_quantize("embedding")::bit(1536)) bit_hamming_ops) WITH (m=16,ef_construction=64);--> statement-breakpoint +-- migration-safe: replay removes only this new candidate index so an interrupted concurrent build cannot remain invalid. +DROP INDEX CONCURRENTLY IF EXISTS "embedding_384_binary_hnsw_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "embedding_384_binary_hnsw_idx" ON "embedding" USING hnsw ((binary_quantize("embedding_384")::bit(384)) bit_hamming_ops) WITH (m=16,ef_construction=64);--> statement-breakpoint +-- migration-safe: replay removes only this new candidate index so an interrupted concurrent build cannot remain invalid. +DROP INDEX CONCURRENTLY IF EXISTS "embedding_768_binary_hnsw_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "embedding_768_binary_hnsw_idx" ON "embedding" USING hnsw ((binary_quantize("embedding_768")::bit(768)) bit_hamming_ops) WITH (m=16,ef_construction=64);--> statement-breakpoint +-- migration-safe: replay removes only this new candidate index so an interrupted concurrent build cannot remain invalid. +DROP INDEX CONCURRENTLY IF EXISTS "embedding_1024_binary_hnsw_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "embedding_1024_binary_hnsw_idx" ON "embedding" USING hnsw ((binary_quantize("embedding_1024")::bit(1024)) bit_hamming_ops) WITH (m=16,ef_construction=64);--> statement-breakpoint +-- migration-safe: replay removes only this new candidate index so an interrupted concurrent build cannot remain invalid. +DROP INDEX CONCURRENTLY IF EXISTS "embedding_3072_binary_hnsw_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "embedding_3072_binary_hnsw_idx" ON "embedding" USING hnsw ((binary_quantize("embedding_3072")::bit(3072)) bit_hamming_ops) WITH (m=16,ef_construction=64);--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/migrations/meta/0342_snapshot.json b/packages/db/migrations/meta/0342_snapshot.json new file mode 100644 index 00000000000..d0382298f77 --- /dev/null +++ b/packages/db/migrations/meta/0342_snapshot.json @@ -0,0 +1,26392 @@ +{ + "id": "a93f8916-30f0-4b9e-ae2f-fe3c3861d848", + "prevId": "38b15e57-1b67-4a30-86e7-12d829535277", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_recovery_idx": { + "name": "doc_processing_recovery_idx", + "columns": [ + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_idx": { + "name": "doc_connector_reconciliation_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"source_seen_at\", '-infinity'::timestamp)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_384_vector_hnsw_idx": { + "name": "embedding_384_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_768_vector_hnsw_idx": { + "name": "embedding_768_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_1024_vector_hnsw_idx": { + "name": "embedding_1024_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_3072_vector_hnsw_idx": { + "name": "embedding_3072_vector_hnsw_idx", + "columns": [ + { + "expression": "(\"embedding_3072\"::halfvec(3072)) halfvec_cosine_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_binary_hnsw_idx": { + "name": "embedding_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding\")::bit(1536)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_384_binary_hnsw_idx": { + "name": "embedding_384_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_384\")::bit(384)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_768_binary_hnsw_idx": { + "name": "embedding_768_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_768\")::bit(768)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_1024_binary_hnsw_idx": { + "name": "embedding_1024_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_1024\")::bit(1024)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_3072_binary_hnsw_idx": { + "name": "embedding_3072_binary_hnsw_idx", + "columns": [ + { + "expression": "(binary_quantize(\"embedding_3072\")::bit(3072)) bit_hamming_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_invocation": { + "name": "organization_search_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_types": { + "name": "source_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_invocation_org_created_idx": { + "name": "organization_search_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_invocation_user_idx": { + "name": "organization_search_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_invocation_organization_id_organization_id_fk": { + "name": "organization_search_invocation_organization_id_organization_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_invocation_user_id_user_id_fk": { + "name": "organization_search_invocation_user_id_user_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_invocation_result_count_bounds": { + "name": "organization_search_invocation_result_count_bounds", + "value": "\"organization_search_invocation\".\"result_count\" BETWEEN 0 AND 100" + }, + "organization_search_invocation_source_types_bounds": { + "name": "organization_search_invocation_source_types_bounds", + "value": "cardinality(\"organization_search_invocation\".\"source_types\") <= 100" + } + }, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_event\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_group_idx": { + "name": "scim_group_mapping_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_org_domain_unique": { + "name": "sso_provider_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(regexp_replace(btrim(\"domain\"), '^\\*\\.', ''))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"sso_provider\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output", + "api-tool" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 89e2f336758..450ef61c1c2 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2388,6 +2388,13 @@ "when": 1789154850550, "tag": "0341_organization_logo_uploads", "breakpoints": true + }, + { + "idx": 342, + "version": "7", + "when": 1789171419865, + "tag": "0342_clean_weapon_omega", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 67ab4b1aa6e..07af0cf9eb9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3269,6 +3269,23 @@ export const embedding = pgTable( ef_construction: 64, }), + /** Permission-filtered searches traverse compact candidates, then rerank the stored vectors. */ + embeddingBinaryHnswIdx: index('embedding_binary_hnsw_idx') + .using('hnsw', sql`(binary_quantize(${table.embedding})::bit(1536)) bit_hamming_ops`) + .with({ m: 16, ef_construction: 64 }), + embedding384BinaryHnswIdx: index('embedding_384_binary_hnsw_idx') + .using('hnsw', sql`(binary_quantize(${table.embedding384})::bit(384)) bit_hamming_ops`) + .with({ m: 16, ef_construction: 64 }), + embedding768BinaryHnswIdx: index('embedding_768_binary_hnsw_idx') + .using('hnsw', sql`(binary_quantize(${table.embedding768})::bit(768)) bit_hamming_ops`) + .with({ m: 16, ef_construction: 64 }), + embedding1024BinaryHnswIdx: index('embedding_1024_binary_hnsw_idx') + .using('hnsw', sql`(binary_quantize(${table.embedding1024})::bit(1024)) bit_hamming_ops`) + .with({ m: 16, ef_construction: 64 }), + embedding3072BinaryHnswIdx: index('embedding_3072_binary_hnsw_idx') + .using('hnsw', sql`(binary_quantize(${table.embedding3072})::bit(3072)) bit_hamming_ops`) + .with({ m: 16, ef_construction: 64 }), + // Text tag indexes tag1Idx: index('emb_kb_tag1_lower_idx').on(table.knowledgeBaseId, sql`lower(${table.tag1})`), tag2Idx: index('emb_kb_tag2_lower_idx').on(table.knowledgeBaseId, sql`lower(${table.tag2})`), From 3bdc2790b304aabfde87f4241132a8209d58e2da Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:24:25 -0700 Subject: [PATCH 07/15] fix(workflows): surface workflow import failures (#7768) * fix(workflows): show errors when workflow imports fail * fix(workflows): clarify invalid import file errors --- .../w/hooks/use-import-workflow.ts | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts index a8f164ed9a7..63074d750f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts @@ -1,5 +1,7 @@ import { useCallback, useRef, useState } from 'react' +import { toast } from '@sim/emcn' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' @@ -34,9 +36,9 @@ interface UseImportWorkflowProps { */ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { const router = useRouter() - const createWorkflowMutation = useCreateWorkflow() + const { mutateAsync: createWorkflow } = useCreateWorkflow() const queryClient = useQueryClient() - const createFolderMutation = useCreateFolder() + const { mutateAsync: createFolder } = useCreateFolder() const clearDiff = useWorkflowDiffStore((state) => state.clearDiff) const posthog = usePostHog() const posthogRef = useRef(posthog) @@ -56,7 +58,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { folderId, sortOrder, createWorkflow: async ({ name, description, workspaceId, folderId, sortOrder }) => - createWorkflowMutation.mutateAsync({ + createWorkflow({ name, description, workspaceId, @@ -66,9 +68,13 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { }), }) - return result?.workflowId ?? null + if (!result) { + throw new Error('The file does not contain valid workflow JSON.') + } + + return result.workflowId }, - [clearDiff, createWorkflowMutation, workspaceId] + [clearDiff, createWorkflow, workspaceId] ) /** @@ -89,10 +95,16 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { if (hasZip && fileArray.length === 1) { const zipFile = fileArray[0] - const { workflows: extractedWorkflows, metadata } = await extractWorkflowsFromZip(zipFile) + const { workflows: extractedWorkflows, metadata } = await extractWorkflowsFromZip( + zipFile + ).catch((error: unknown) => { + throw new Error('Failed to import ZIP file. Check that it is a valid ZIP archive.', { + cause: error, + }) + }) const folderName = metadata?.workspaceName || zipFile.name.replace(/\.zip$/i, '') - const importFolder = await createFolderMutation.mutateAsync({ + const importFolder = await createFolder({ name: folderName, workspaceId, }) @@ -131,7 +143,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { parentId = await createFolderRecursive(foldersById.get(folder.parentId)!) } - const newFolder = await createFolderMutation.mutateAsync({ + const newFolder = await createFolder({ name: folder.name, workspaceId, parentId, @@ -163,7 +175,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { const folderNameForSegment = workflow.folderPath[i] if (!folderMap.has(pathSegment)) { - const subFolder = await createFolderMutation.mutateAsync({ + const subFolder = await createFolder({ name: folderNameForSegment, workspaceId, parentId, @@ -187,6 +199,9 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { if (workflowId) importedWorkflowIds.push(workflowId) } catch (error) { logger.error(`Failed to import ${workflow.name}:`, error) + toast.error(`Failed to import ${workflow.name}`, { + description: getErrorMessage(error, 'Workflow import failed'), + }) } } } else if (jsonFiles.length > 0) { @@ -198,6 +213,9 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { if (workflowId) importedWorkflowIds.push(workflowId) } catch (error) { logger.error(`Failed to import ${workflow.name}:`, error) + toast.error(`Failed to import ${workflow.name}`, { + description: getErrorMessage(error, 'Workflow import failed'), + }) } } } @@ -219,6 +237,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { } } catch (error) { logger.error('Failed to import workflows:', error) + toast.error(getErrorMessage(error, 'Failed to import workflows')) } finally { setIsImporting(false) @@ -227,7 +246,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { } } }, - [importSingleWorkflow, workspaceId, router, createFolderMutation, queryClient] + [importSingleWorkflow, workspaceId, router, createFolder, queryClient] ) return { From 1a7c827d8323760e69712d93311119f9036b5a80 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:02:58 -0700 Subject: [PATCH 08/15] fix(realtime): compare search replacements independent of object key order (#7769) --- apps/realtime/src/database/operations.test.ts | 123 ++++++++++++++++++ apps/realtime/src/database/operations.ts | 8 +- 2 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 apps/realtime/src/database/operations.test.ts diff --git a/apps/realtime/src/database/operations.test.ts b/apps/realtime/src/database/operations.test.ts new file mode 100644 index 00000000000..061a37af21d --- /dev/null +++ b/apps/realtime/src/database/operations.test.ts @@ -0,0 +1,123 @@ +/** @vitest-environment node */ +import { OPERATION_TARGETS, SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockTransaction, mockSelectWhere, mockSet } = vi.hoisted(() => ({ + mockTransaction: vi.fn(), + mockSelectWhere: vi.fn(), + mockSet: vi.fn(), +})) + +vi.mock('@sim/audit', () => ({ AuditAction: {}, AuditResourceType: {}, recordAudit: vi.fn() })) +vi.mock('@sim/db', () => ({ + instrumentPoolClient: vi.fn(), + resolveDbUrl: vi.fn(() => 'postgres://localhost/test'), + workflow: { id: 'workflow.id' }, + workflowBlocks: { id: 'block.id', workflowId: 'block.workflowId' }, + workflowEdges: {}, + workflowSubflows: {}, +})) +vi.mock('@sim/db/timestamps', () => ({ withUtcTimestamps: (options: unknown) => options })) +vi.mock('@sim/logger', () => ({ + createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }), +})) +vi.mock('@sim/platform-authz/workflow', () => ({ + getActiveWorkflowContext: vi.fn().mockResolvedValue({ id: 'workflow-1' }), +})) +vi.mock('@sim/workflow-persistence/load', () => ({ + loadWorkflowFromNormalizedTablesRaw: vi.fn(), +})) +vi.mock('@sim/workflow-persistence/subblocks', () => ({ mergeSubBlockValues: vi.fn() })) +vi.mock('drizzle-orm', () => ({ + and: vi.fn(), + eq: vi.fn(), + inArray: vi.fn(), + isNull: vi.fn(), + or: vi.fn(), + sql: vi.fn(), +})) +vi.mock('drizzle-orm/postgres-js', () => ({ drizzle: () => ({ transaction: mockTransaction }) })) +vi.mock('postgres', () => ({ default: vi.fn() })) +vi.mock('@/env', () => ({ + env: { DATABASE_URL: 'postgres://localhost/test' }, +})) + +import { persistWorkflowOperation } from '@/database/operations' + +const transaction = { + select: () => ({ from: () => ({ where: mockSelectWhere }) }), + update: () => ({ set: mockSet }), + delete: vi.fn(), + insert: vi.fn(), +} + +describe('search replacement persistence', () => { + const expected = [ + { + type: 'function', + params: { language: 'javascript', code: 'return 1' }, + usageControl: 'none', + }, + ] + const replacement = [{ ...expected[0], params: { ...expected[0].params, code: 'return 2' } }] + + beforeEach(() => { + vi.clearAllMocks() + mockTransaction.mockImplementation( + async (callback: (tx: typeof transaction) => Promise) => callback(transaction) + ) + mockSet.mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }) + }) + + function replaceTools(stored: unknown, expectedValue: unknown = expected) { + mockSelectWhere.mockResolvedValue([ + { + id: 'agent-1', + type: 'agent', + locked: false, + data: {}, + subBlocks: { tools: { id: 'tools', type: 'tool-input', value: stored } }, + }, + ]) + return persistWorkflowOperation('workflow-1', { + operation: SUBBLOCK_OPERATIONS.BATCH_UPDATE, + target: OPERATION_TARGETS.SUBBLOCK, + timestamp: Date.now(), + payload: { + updates: [{ blockId: 'agent-1', subblockId: 'tools', value: replacement, expectedValue }], + }, + }) + } + + it('accepts equivalent nested tool objects after JSONB changes their key order', async () => { + const stored = [ + { + usageControl: 'none', + params: { code: 'return 1', language: 'javascript' }, + type: 'function', + }, + ] + + await expect(replaceTools(stored)).resolves.toBeUndefined() + expect(mockSet).toHaveBeenLastCalledWith( + expect.objectContaining({ + subBlocks: { tools: { id: 'tools', type: 'tool-input', value: replacement } }, + }) + ) + }) + + it('still rejects a tool parameter changed by another editor', async () => { + await expect( + replaceTools([{ ...expected[0], params: { ...expected[0].params, code: 'return 3' } }]) + ).rejects.toThrow('changed since replacement was planned') + expect(mockSet).toHaveBeenCalledTimes(1) + }) + + it('still rejects reordered tool arrays', async () => { + const another = { ...expected[0], params: { ...expected[0].params, code: 'return 3' } } + await expect(replaceTools([another, expected[0]], [expected[0], another])).rejects.toThrow( + 'changed since replacement was planned' + ) + expect(mockSet).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/realtime/src/database/operations.ts b/apps/realtime/src/database/operations.ts index f28543a8004..777c6078316 100644 --- a/apps/realtime/src/database/operations.ts +++ b/apps/realtime/src/database/operations.ts @@ -1,3 +1,4 @@ +import { isDeepStrictEqual } from 'node:util' import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import * as schema from '@sim/db' import { @@ -1988,10 +1989,6 @@ async function handleSubflowOperationTx( } } -function valuesEqual(left: unknown, right: unknown): boolean { - return JSON.stringify(left) === JSON.stringify(right) -} - // Subblock operations - targeted value updates without replacing workflow state async function handleSubblockOperationTx( tx: any, @@ -2039,7 +2036,8 @@ async function handleSubblockOperationTx( const subBlocks = { ...((block.subBlocks as Record) || {}) } const currentSubBlock = subBlocks[subblockId] const currentValue = currentSubBlock?.value - if (expectedValue !== undefined && !valuesEqual(currentValue, expectedValue)) { + /** JSONB can reorder object keys; changed values and array order must still conflict. */ + if (expectedValue !== undefined && !isDeepStrictEqual(currentValue, expectedValue)) { throw new Error(`Subblock ${blockId}.${subblockId} changed since replacement was planned`) } From 2799994574c2a71f4caec3292314f68e5db794e9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 12 Sep 2026 19:53:16 -0700 Subject: [PATCH 09/15] fix(desktop): reopen a chat on the browser tab the user left it on (#7793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(desktop): reopen a chat on the browser tab the user left it on The resource strip pushed its own last-tab fallback onto the desktop app whenever a chat opened without an explicit selection, overriding the tab the desktop remembers the user was on. The shared desktop-tab hook now switches the native tab only for an explicit selection, adopts the desktop's active tab when the strip is on its fallback, and defers a selected tab that has not landed yet until it does. Chat hydration no longer writes a browser or terminal tab into the URL as a fallback. * fix(desktop): adopt the remembered tab without claiming the user's selection Review round on the reopen fix. A late first report of the desktop app's active tab carries the tab it remembers, not a switch the user made, so it is adopted rather than claimed and agent activity can still take the view on chat open. A move away from a tab the desktop was already showing stays the user's own. Adoption now waits for the chat history to be applied, so the arrival order of the tab list and the history no longer decides which resource a chat opens on, and it skips a tab the strip has already dropped, so closing the shown tab cannot write the closed id back. Closing the shown tab selects its neighbour the way the desktop app picks the next native tab, instead of flashing through the strip's last tab. The two wrapper hooks now share one options type with the strip, and the adopt rule lives in a single helper. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0139YonWmiZUnPMTHoH4PtAJ * fix(desktop): show a selected tab that arrives after the tab list The effect that shows an explicitly selected tab was keyed on the selection alone, so a selection made before the desktop app published its tab list was dropped rather than applied when the tab arrived. It is now keyed on that tab being live as well, which covers the late arrival without a retry ref to arm and disarm. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0139YonWmiZUnPMTHoH4PtAJ * refactor(desktop): align the two tab-adoption paths and drop dead plumbing Quality pass on the reopen fix. The late-arrival adoption now carries the same guards as the hydrated one, so a first report of the desktop app's active tab can no longer override a selection the user made before the tab list arrived. Both guards are pinned by tests that fail when either is removed. The predicate the adopt and claim paths share moved into one helper, so the single difference between them — adoption needs the tab to still be in the strip, following the user does not — is stated once. Removes a ref nothing read and an options interface with no second consumer. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0139YonWmiZUnPMTHoH4PtAJ * test(desktop): give the tab-resource hosts the shared options interface The test hosts took their props through a type alias derived from the hook signature. The repo asks for an interface, and the hook already exports one that is exactly this shape, so the hosts use it directly instead of restating it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0139YonWmiZUnPMTHoH4PtAJ * fix(desktop): resolve the shown tab instead of writing it back Reopening a chat could show the wrong page for as long as the chat history took to load. The strip wrote the desktop app's remembered tab into the selection from an effect, and that write had to wait for the history or the arrival order would decide what the chat opened on. With the history held back 2.5s, an instrumented run showed the wrong page for 2164ms before it corrected. The rule is now a pure function: an explicit selection wins, otherwise the last resource, except that a desktop-backed last resource defers to the tab the desktop app is showing. Nothing is written back, so the gate, the passive setter and the effect behind them are gone, and the same run now shows the remembered page immediately. A native switch is claimed as the user's against the tab the desktop app was showing rather than the one the strip shows, since with no explicit selection those are now the same tab. Closing the shown tab prefers a neighbour of its own kind, so the strip and the desktop app agree on what comes next. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0139YonWmiZUnPMTHoH4PtAJ --------- Co-authored-by: Claude Opus 5 (1M context) --- .../resource-tabs/resource-tabs.tsx | 23 +++- .../app/workspace/[workspaceId]/home/home.tsx | 24 ++-- .../hooks/use-browser-tab-resources.test.tsx | 83 +++++++++++--- .../home/hooks/use-browser-tab-resources.ts | 29 +---- .../[workspaceId]/home/hooks/use-chat.ts | 81 ++++++++----- .../home/hooks/use-desktop-tab-resources.ts | 106 +++++++++++++----- .../hooks/use-terminal-tab-resources.test.tsx | 38 ++++--- .../home/hooks/use-terminal-tab-resources.ts | 33 +----- .../home/resource-view-policy.test.ts | 63 +++++++++++ .../home/resource-view-policy.ts | 40 +++++++ 10 files changed, 358 insertions(+), 162 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index 66d91c0e0be..9f1d18d94ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -408,11 +408,21 @@ export function ResourceTabs({ const handleClose = useCallback( (id: string) => { - const resource = resources.find((r) => r.id === id) + const index = resources.findIndex((r) => r.id === id) + const resource = resources[index] if (!resource) return const isMulti = selectedIds.has(resource.id) && selectedIds.size > 1 const targets = isMulti ? resources.filter((r) => selectedIds.has(r.id)) : [resource] if (!confirmClosingRunningTerminals(targets, terminalTabs)) return + // Closing the shown tab moves to its neighbour, right then left, so the + // strip does not fall back to its last tab and jump. For a desktop tab + // this is also the neighbour the desktop app itself picks. + if (!isMulti && activeId === resource.id) { + const sameKind = new Set(resources.filter((r) => r.type === resource.type).map((r) => r.id)) + const nextId = + findNearestId(resources, index, sameKind) ?? findNearestId(resources, index, null) + if (nextId) selectResource(nextId) + } // A browser tab's page is closed natively and its resource dropped at // once; the tab list then confirms the removal. A shell's close answers // with the tab list, so its resource follows that list instead — a @@ -451,7 +461,16 @@ export function ResourceTabs({ } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [chatId, desktopScopeId, onRemoveResource, resources, selectedIds, terminalTabs] + [ + activeId, + chatId, + desktopScopeId, + onRemoveResource, + resources, + selectResource, + selectedIds, + terminalTabs, + ] ) /** diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index c169af084e8..05e78e0d2f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -224,7 +224,7 @@ export function Home({ chatId, userName, userId }: HomeProps) { const resourceSelectionOwnedByUserRef = useRef(false) function handleResourceEvent(resourceId: string, options?: ResourceEventOptions) { - const activeResourceId = activeResourceParamRef.current + const activeResourceId = effectiveActiveResourceIdRef.current const presentation = resolveResourceEventPresentation({ activeResourceId, activationRequested: shouldActivateResourceEvent(activeResourceId, resourceId, options), @@ -317,7 +317,7 @@ export function Home({ chatId, userName, userId }: HomeProps) { const expandResource = () => { resourceCollapseOwnedByUserRef.current = false resourceSelectionOwnedByUserRef.current = true - const activeResourceId = activeResourceParamRef.current + const activeResourceId = effectiveActiveResourceIdRef.current if (activeResourceId) clearResourceActivity(activeResourceId) setResourceCollapsed(false) } @@ -334,24 +334,18 @@ export function Home({ chatId, userName, userId }: HomeProps) { [setActiveResourceId, clearResourceActivity] ) - const desktopTabResourceCallbacks = { + const desktopTabResourceOptions = { + scopeId: desktopScopeId, + resources, + activeResourceId, + selectedResourceId: activeResourceParam, addResource, removeResource, selectResource: selectResourceFromUser, onResourceEvent: handleResourceEvent, } - useBrowserTabResources({ - scopeId: desktopScopeId, - resources, - activeResourceId, - ...desktopTabResourceCallbacks, - }) - useTerminalTabResources({ - scopeId: desktopScopeId, - resources, - activeResourceId, - ...desktopTabResourceCallbacks, - }) + useBrowserTabResources(desktopTabResourceOptions) + useTerminalTabResources(desktopTabResourceOptions) const addResourceFromUser = useCallback( (resource: MothershipResource) => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.test.tsx index 2563e158eac..274b908ab54 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.test.tsx @@ -6,6 +6,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { MothershipResource } from '@/lib/copilot/resources/types' import { useBrowserTabResources } from '@/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources' +import type { DesktopTabResourceOptions } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources' import { useBrowserSessionStore } from '@/stores/browser-session/store' const { sendBrowserPanelAction, openUrlInNewBrowserTab, openInPanelListeners } = vi.hoisted(() => ({ @@ -37,17 +38,7 @@ function pushTabs(scopeId: string, tabs: ReturnType[], activeTabId: }) } -interface HostProps { - scopeId: string - resources: MothershipResource[] - activeResourceId: string | null - addResource: (resource: MothershipResource) => void - removeResource: (type: MothershipResource['type'], id: string) => void - selectResource: (id: string) => void - onResourceEvent: (id: string, options?: { activate?: boolean }) => void -} - -function Host(props: HostProps) { +function Host(props: DesktopTabResourceOptions) { useBrowserTabResources(props) return null } @@ -60,11 +51,12 @@ describe('useBrowserTabResources', () => { const selectResource = vi.fn() const onResourceEvent = vi.fn() - function render(overrides: Partial = {}) { - const props: HostProps = { + function render(overrides: Partial = {}) { + const props: DesktopTabResourceOptions = { scopeId: SCOPE, resources: [], activeResourceId: null, + selectedResourceId: null, addResource, removeResource, selectResource, @@ -72,7 +64,12 @@ describe('useBrowserTabResources', () => { ...overrides, } act(() => root.render()) - return (next: Partial) => act(() => root.render()) + /** `alsoInThisCommit` lands a store push and the new props together. */ + return (next: Partial, alsoInThisCommit?: () => void) => + act(() => { + alsoInThisCommit?.() + root.render() + }) } beforeEach(() => { @@ -153,11 +150,11 @@ describe('useBrowserTabResources', () => { { type: 'browser', id: '1', title: 'Page 1' }, { type: 'browser', id: '2', title: 'Page 2' }, ] - const rerender = render({ resources, activeResourceId: '1' }) + const rerender = render({ resources, activeResourceId: '1', selectedResourceId: '1' }) pushTabs(SCOPE, [tab('1', true), tab('2')], '1') expect(sendBrowserPanelAction).not.toHaveBeenCalled() - rerender({ activeResourceId: '2' }) + rerender({ activeResourceId: '2', selectedResourceId: '2' }) expect(sendBrowserPanelAction).toHaveBeenCalledExactlyOnceWith( 'switch-tab', { tabId: '2', claim: false }, @@ -169,13 +166,62 @@ describe('useBrowserTabResources', () => { expect(selectResource).not.toHaveBeenCalled() }) + it('shows a page selected before the pages landed, once it arrives', () => { + render({ selectedResourceId: '2', activeResourceId: '2' }) + expect(sendBrowserPanelAction).not.toHaveBeenCalled() + + pushTabs(SCOPE, [tab('1', true), tab('2')], '1') + expect(sendBrowserPanelAction).toHaveBeenCalledExactlyOnceWith( + 'switch-tab', + { tabId: '2', claim: false }, + SCOPE + ) + }) + + it('does not claim the scope first report as a user switch', () => { + const resources: MothershipResource[] = [ + { type: 'browser', id: '1', title: 'Page 1' }, + { type: 'browser', id: '2', title: 'Page 2' }, + ] + const rerender = render() + pushTabs(SCOPE, [tab('1'), tab('2')], null) + rerender({ resources, activeResourceId: '2', selectedResourceId: null }) + + // The desktop app reports the page it restored. The strip resolves to that + // page on its own, so there is nothing here to claim for the user. + pushTabs(SCOPE, [tab('1', true), tab('2')], '1') + expect(selectResource).not.toHaveBeenCalled() + expect(sendBrowserPanelAction).not.toHaveBeenCalled() + }) + + it('claims a native switch away from a page it was already showing', () => { + const resources: MothershipResource[] = [ + { type: 'browser', id: '1', title: 'Page 1' }, + { type: 'browser', id: '2', title: 'Page 2' }, + ] + const rerender = render() + pushTabs(SCOPE, [tab('1', true), tab('2')], '1') + rerender({ resources, activeResourceId: '1', selectedResourceId: null }) + expect(selectResource).not.toHaveBeenCalled() + + // A keyboard shortcut in the page moves the desktop app to page 2. With no + // explicit selection the strip resolves to that page in the same commit, + // so the switch is only visible against the page the desktop app left. + rerender({ resources, activeResourceId: '2' }, () => { + useBrowserSessionStore + .getState() + .setTabsState({ scopeId: SCOPE, tabs: [tab('1'), tab('2', true)], activeTabId: '2' }) + }) + expect(selectResource).toHaveBeenCalledExactlyOnceWith('2') + }) + it('follows a native switch into the strip only while the user is on the browser', () => { const resources: MothershipResource[] = [ { type: 'browser', id: '1', title: 'Page 1' }, { type: 'browser', id: '2', title: 'Page 2' }, { type: 'file', id: 'f', title: 'notes.md' }, ] - const rerender = render({ resources, activeResourceId: '1' }) + const rerender = render({ resources, activeResourceId: '1', selectedResourceId: '1' }) pushTabs(SCOPE, [tab('1', true), tab('2')], '1') pushTabs(SCOPE, [tab('1'), tab('2', true)], '2') @@ -183,7 +229,7 @@ describe('useBrowserTabResources', () => { expect(sendBrowserPanelAction).not.toHaveBeenCalled() selectResource.mockClear() - rerender({ activeResourceId: 'f' }) + rerender({ activeResourceId: 'f', selectedResourceId: 'f' }) pushTabs(SCOPE, [tab('1', true), tab('2')], '1') expect(selectResource).not.toHaveBeenCalled() }) @@ -192,6 +238,7 @@ describe('useBrowserTabResources', () => { render({ resources: [{ type: 'browser', id: '1', title: 'Page 1' }], activeResourceId: '1', + selectedResourceId: '1', }) pushTabs(SCOPE, [tab('1', true)], '1') act(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.ts index 51885bd161f..93fdb488d8a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-browser-tab-resources.ts @@ -5,9 +5,8 @@ import { getErrorMessage } from '@sim/utils/errors' import { onOpenInBrowserPanel } from '@/lib/browser-agent/open-in-panel' import { browserTabTitle } from '@/lib/browser-agent/tab-label' import { openUrlInNewBrowserTab, sendBrowserPanelAction } from '@/lib/browser-agent/transport' -import type { MothershipResource } from '@/lib/copilot/resources/types' import { - type DesktopTabResourceCallbacks, + type DesktopTabResourceOptions, useDesktopTabResources, } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources' import { useBrowserSessionStore } from '@/stores/browser-session/store' @@ -16,13 +15,6 @@ const logger = createLogger('BrowserTabResources') const EMPTY_BROWSER_TABS: BrowserTabState[] = [] -interface UseBrowserTabResourcesOptions extends DesktopTabResourceCallbacks { - /** Desktop browser scope whose pages back this chat's browser tabs. */ - scopeId: string - resources: readonly MothershipResource[] - activeResourceId: string | null -} - function switchBrowserTab(tabId: string, scopeId: string): void { sendBrowserPanelAction('switch-tab', { tabId, claim: false }, scopeId) } @@ -31,15 +23,8 @@ function switchBrowserTab(tabId: string, scopeId: string): void { * Projects the desktop app's live browser pages into `browser` resource tabs, * one per page. See {@link useDesktopTabResources} for the shared model. */ -export function useBrowserTabResources({ - scopeId, - resources, - activeResourceId, - addResource, - removeResource, - selectResource, - onResourceEvent, -}: UseBrowserTabResourcesOptions): void { +export function useBrowserTabResources(options: DesktopTabResourceOptions): void { + const { scopeId, selectResource } = options const hasSession = useBrowserSessionStore((state) => state.sessions[scopeId] !== undefined) const browserTabs = useBrowserSessionStore( (state) => state.sessions[scopeId]?.tabs ?? EMPTY_BROWSER_TABS @@ -64,19 +49,13 @@ export function useBrowserTabResources({ selectResourceRef.current = selectResource useDesktopTabResources({ + ...options, type: 'browser', - scopeId, tabs, hasSession, activeTabId, agentTabId, switchTab: switchBrowserTab, - resources, - activeResourceId, - addResource, - removeResource, - selectResource, - onResourceEvent, }) // Chat links clicked in the desktop app open in a new browser tab. The user diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 0ac26689ac1..549a636aefa 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -117,6 +117,8 @@ import { dispatchStreamEvent, finalizeResidualToolCalls, } from '@/app/workspace/[workspaceId]/home/hooks/stream' +import { useNativeActiveTabIds } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources' +import { resolveEffectiveResourceId } from '@/app/workspace/[workspaceId]/home/resource-view-policy' import { fetchMothershipChatHistory, type MothershipChatHistory, @@ -1306,6 +1308,12 @@ export interface UseChatOptions { * selection intentionally stays out of the URL. */ activeResourceState?: [string | null, Dispatch>] + /** + * Whether this surface projects the desktop app's browser and terminal tabs + * into its resources. Only then does the shown resource depend on which tab + * the desktop app displays. + */ + projectsDesktopTabs?: boolean /** Fired when the server's `traceparent` response header arrives, before any stream content. */ onRequestStarted?: (info: { requestId: string; userMessageId: string }) => void } @@ -1335,6 +1343,7 @@ export function getMothershipUseChatOptions( return { apiPath: MOTHERSHIP_CHAT_API_PATH, stopPath: '/api/mothership/chat/stop', + projectsDesktopTabs: true, ...options, } } @@ -1446,14 +1455,33 @@ export function useChat( const pendingResourceReordersRef = useRef(new Map()) const pendingResourceReorderFlushesRef = useRef(new Map>()) - // Derive the effective active resource ID for rendering without writing a - // passive fallback back into the user's URL selection. - const effectiveActiveResourceId = useMemo(() => { - if (resources.length === 0) return null - if (activeResourceId && resources.some((r) => r.id === activeResourceId)) - return activeResourceId - return resources[resources.length - 1].id - }, [resources, activeResourceId]) + // Sentinel used while no `chatId` is resolved; `adoptResolvedChatId` + // migrates this bucket onto the real chatId on first send. Rotated on + // home reset so a new pending chat starts with an empty bucket. + const pendingChatKeyRef = useRef(`${PENDING_CHAT_KEY_PREFIX}${generateShortId()}`) + const pendingDesktopScopeIdRef = useRef( + desktopChatScopeId(scopeKey, undefined, pendingChatKeyRef.current) + ) + const initialDesktopScopeId = desktopChatScopeId( + scopeKey, + initialChatId, + pendingChatKeyRef.current + ) + const desktopScopeIdRef = useRef(initialDesktopScopeId) + const [desktopScopeId, setDesktopScopeId] = useState(initialDesktopScopeId) + const nativeActiveTabIds = useNativeActiveTabIds( + options?.projectsDesktopTabs ? desktopScopeId : null + ) + + const nativeActiveTabIdsRef = useRef(nativeActiveTabIds) + nativeActiveTabIdsRef.current = nativeActiveTabIds + + // Derived for rendering rather than written back, so nothing the user did not + // choose ever lands in their selection. + const effectiveActiveResourceId = useMemo( + () => resolveEffectiveResourceId(resources, activeResourceId, nativeActiveTabIds), + [resources, activeResourceId, nativeActiveTabIds] + ) const activeResourceIdRef = useRef(effectiveActiveResourceId) activeResourceIdRef.current = effectiveActiveResourceId @@ -1502,10 +1530,6 @@ export function useChat( [queryClient] ) - // Sentinel used while no `chatId` is resolved; `adoptResolvedChatId` - // migrates this bucket onto the real chatId on first send. Rotated on - // home reset so a new pending chat starts with an empty bucket. - const pendingChatKeyRef = useRef(`${PENDING_CHAT_KEY_PREFIX}${generateShortId()}`) const [chatKey, setChatKey] = useState(initialChatId ?? pendingChatKeyRef.current) const chatKeyRef = useRef(chatKey) chatKeyRef.current = chatKey @@ -1567,16 +1591,6 @@ export function useChat( const detachedChatResolutionControllersRef = useRef>(new Set()) const streamReaderRef = useRef | null>(null) const chatIdRef = useRef(initialChatId) - const pendingDesktopScopeIdRef = useRef( - desktopChatScopeId(scopeKey, undefined, pendingChatKeyRef.current) - ) - const initialDesktopScopeId = desktopChatScopeId( - scopeKey, - initialChatId, - pendingChatKeyRef.current - ) - const desktopScopeIdRef = useRef(initialDesktopScopeId) - const [desktopScopeId, setDesktopScopeId] = useState(initialDesktopScopeId) /** Panel/chat selection — drives createNewChat + request chatId; may differ from chatIdRef while a stream is still finishing. */ const selectedChatIdRef = useRef(initialChatId) selectedChatIdRef.current = initialChatId @@ -2476,22 +2490,27 @@ export function useChat( ) if (mergedResources.length > 0) { - // An explicit selection wins. Otherwise fall back to the last resource - // the server holds, not the last on screen: local-only browser tabs can - // land before the history does, and which side arrives first must not - // decide which tab the chat opens on. + // An explicit selection wins. Otherwise pin the last resource the server + // holds, not the last on screen: local-only browser tabs can land before + // the history does, and which side arrives first must not decide which + // tab the chat opens on. When the server holds nothing it writes no + // fallback at all: the selection stays empty so the shown resource can be + // resolved against the tab the desktop app remembers. const selectedResourceId = selectedResourceIdRef.current const hydratedActiveResourceId = selectedResourceId && mergedResources.some((resource) => resource.id === selectedResourceId) ? selectedResourceId - : ( - restorableResources[restorableResources.length - 1] ?? - mergedResources[mergedResources.length - 1] - ).id + : (restorableResources[restorableResources.length - 1]?.id ?? null) // Replacing the array with an identical one still re-renders the tab // strip and panel — skip the no-op so open panels don't flash. if (!resourcesUnchanged) { - activeResourceIdRef.current = hydratedActiveResourceId + // The ref is set eagerly so a request sent in this commit still + // attaches a resource, through the same rule the render path uses. + activeResourceIdRef.current = resolveEffectiveResourceId( + mergedResources, + hydratedActiveResourceId, + nativeActiveTabIdsRef.current + ) setResources(mergedResources) setActiveResourceId(hydratedActiveResourceId) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources.ts index 865ad133f15..a2a06067d1d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources.ts @@ -1,6 +1,29 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import type { MothershipResource, MothershipResourceType } from '@/lib/copilot/resources/types' +import { terminalResourceId } from '@/lib/terminal/resource-id' import type { ResourceEventHandler } from '@/app/workspace/[workspaceId]/home/hooks/use-chat' +import type { NativeActiveTabIds } from '@/app/workspace/[workspaceId]/home/resource-view-policy' +import { useBrowserSessionStore } from '@/stores/browser-session/store' +import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store' + +/** + * The tab the desktop app currently shows for a chat, per kind, as resource + * ids. The strip prefers these over its own last-resource fallback. Pass null + * from a surface that projects no desktop tabs, so it never re-renders for a + * native switch it cannot show. + */ +export function useNativeActiveTabIds(scopeId: string | null): NativeActiveTabIds { + const browser = useBrowserSessionStore((state) => + scopeId === null ? null : (state.sessions[scopeId]?.activeTabId ?? null) + ) + const terminal = useCopilotTerminalStore((state) => + scopeId === null ? null : (state.sessions[scopeId]?.tabs.activeTerminalId ?? null) + ) + return useMemo( + () => ({ browser, terminal: terminal ? terminalResourceId(terminal) : null }), + [browser, terminal] + ) +} /** One live desktop tab, as the strip needs to know it. */ export interface DesktopTab { @@ -8,7 +31,15 @@ export interface DesktopTab { title: string } -export interface DesktopTabResourceCallbacks { +/** What the strip shares with every kind of desktop-backed resource tab. */ +export interface DesktopTabResourceOptions { + /** Desktop scope whose live tabs back this chat's resource tabs. */ + scopeId: string + resources: readonly MothershipResource[] + /** The resource the strip shows: the explicit selection or its fallback. */ + activeResourceId: string | null + /** The explicit selection alone, without the strip's fallback. */ + selectedResourceId: string | null /** Adds a tab without activating it; activation goes through {@link onResourceEvent}. */ addResource: (resource: MothershipResource) => void removeResource: (resourceType: MothershipResourceType, resourceId: string) => void @@ -18,10 +49,8 @@ export interface DesktopTabResourceCallbacks { onResourceEvent: ResourceEventHandler } -interface UseDesktopTabResourcesOptions extends DesktopTabResourceCallbacks { +interface UseDesktopTabResourcesOptions extends DesktopTabResourceOptions { type: 'browser' | 'terminal' - /** Desktop scope whose live tabs back this chat's resource tabs. */ - scopeId: string /** The desktop app's live tab list for the scope, in its order. */ tabs: readonly DesktopTab[] /** @@ -36,8 +65,15 @@ interface UseDesktopTabResourcesOptions extends DesktopTabResourceCallbacks { agentTabId: string | null /** Shows a tab natively without claiming it for the user. */ switchTab: (tabId: string, scopeId: string) => void - resources: readonly MothershipResource[] - activeResourceId: string | null +} + +/** Whether the resource the strip shows is a tab of this kind. */ +function stripShowsKind( + resources: readonly MothershipResource[], + activeResourceId: string | null, + type: MothershipResourceType +): boolean { + return resources.find((resource) => resource.id === activeResourceId)?.type === type } /** @@ -49,7 +85,9 @@ interface UseDesktopTabResourcesOptions extends DesktopTabResourceCallbacks { * a resource tab closes its native tab at the strip, which then comes back * through the same list. Visible selection is routed the same way — choosing * a resource tab switches the native tab, and a native switch follows into the - * strip while the user is on that kind of tab. + * strip while the user is on that kind of tab. Which resource the strip shows + * when nothing is selected is resolved by `resolveEffectiveResourceId`, not + * here. * * The agent never moves the visible tab itself. Its tab is announced as * resource activity, so the existing view policy decides whether to show it or @@ -65,6 +103,7 @@ export function useDesktopTabResources({ switchTab, resources, activeResourceId, + selectedResourceId, addResource, removeResource, selectResource, @@ -83,14 +122,18 @@ export function useDesktopTabResources({ const requestedTabIdRef = useRef(null) const scopeIdRef = useRef(scopeId) scopeIdRef.current = scopeId - const tabsRef = useRef(tabs) - tabsRef.current = tabs const activeTabIdRef = useRef(activeTabId) activeTabIdRef.current = activeTabId const resourcesRef = useRef(resources) resourcesRef.current = resources const activeResourceIdRef = useRef(activeResourceId) activeResourceIdRef.current = activeResourceId + /** + * The last tab the desktop app reported showing. Unset until its first + * report, which carries the tab it remembers rather than a switch. Never + * unset again by an empty tab list, so reopening a tab still reads as a move. + */ + const previousActiveTabIdRef = useRef(null) const switchTabRef = useRef(switchTab) switchTabRef.current = switchTab const selectResourceRef = useRef(selectResource) @@ -105,6 +148,7 @@ export function useDesktopTabResources({ knownScopeRef.current = scopeId known.clear() requestedTabIdRef.current = null + previousActiveTabIdRef.current = null } const resourceTabIds = new Set( resources.filter((resource) => resource.type === type).map((resource) => resource.id) @@ -127,29 +171,39 @@ export function useDesktopTabResources({ } }, [addResource, hasSession, removeResource, resources, scopeId, tabs, type]) - // Selecting a resource tab shows its native tab. Keyed on the selection - // alone: a native push must not re-assert a selection it just moved away - // from, or the two sides would trade switches forever. + const selectedTabIsLive = + selectedResourceId !== null && tabs.some((tab) => tab.id === selectedResourceId) + + // Selecting a resource tab shows its native tab. Keyed on the explicit + // selection alone — the strip's fallback is not a choice to impose on the + // desktop app, and a native push must not re-assert a selection it just + // moved away from, or the two sides would trade switches forever — and on + // that tab being live, so a selection made before the desktop app published + // its tab list is shown once the tab arrives rather than dropped. useEffect(() => { - if (!activeResourceId || activeResourceId === activeTabIdRef.current) return - if (!tabsRef.current.some((tab) => tab.id === activeResourceId)) return - requestedTabIdRef.current = activeResourceId - switchTabRef.current(activeResourceId, scopeIdRef.current) - }, [activeResourceId]) + if (!selectedResourceId || !selectedTabIsLive) return + if (selectedResourceId === activeTabIdRef.current) return + requestedTabIdRef.current = selectedResourceId + switchTabRef.current(selectedResourceId, scopeIdRef.current) + }, [selectedResourceId, selectedTabIsLive]) - // A native switch while the user is on this kind of tab follows into the - // strip. The switch this hook requested itself is not a native change of mind. + // A native switch while the user is on this kind of tab claims the selection + // the way a click on the tab would, so later agent activity only badges + // rather than taking the view. Measured against the tab the desktop app was + // showing, not the one the strip shows: with no explicit selection those are + // the same tab, and comparing them would never see a switch. Two switches + // are not the user's — the one this hook requested itself, and the scope's + // first report, which carries the tab the desktop app remembers. useEffect(() => { + const previousActiveTabId = previousActiveTabIdRef.current + if (activeTabId !== null) previousActiveTabIdRef.current = activeTabId if (requestedTabIdRef.current === activeTabId) { requestedTabIdRef.current = null return } - const activeResource = resourcesRef.current.find( - (resource) => resource.id === activeResourceIdRef.current - ) - if (!activeTabId || activeResource?.type !== type || activeResource.id === activeTabId) { - return - } + if (!activeTabId || previousActiveTabId === null) return + if (activeTabId === previousActiveTabId) return + if (!stripShowsKind(resourcesRef.current, activeResourceIdRef.current, type)) return selectResourceRef.current(activeTabId) }, [activeTabId, type]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.test.tsx index ef79b3ed7c2..013a36d540d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.test.tsx @@ -6,6 +6,7 @@ import type { TerminalTabState } from '@sim/terminal-protocol' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { MothershipResource } from '@/lib/copilot/resources/types' +import type { DesktopTabResourceOptions } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources' import { useTerminalTabResources } from '@/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources' import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store' @@ -38,17 +39,7 @@ function pushTabs(scopeId: string, tabs: TerminalTabState[], activeTerminalId: s }) } -interface HostProps { - scopeId: string - resources: MothershipResource[] - activeResourceId: string | null - addResource: (resource: MothershipResource) => void - removeResource: (type: MothershipResource['type'], id: string) => void - selectResource: (id: string) => void - onResourceEvent: (id: string, options?: { activate?: boolean }) => void -} - -function Host(props: HostProps) { +function Host(props: DesktopTabResourceOptions) { useTerminalTabResources(props) return null } @@ -61,11 +52,12 @@ describe('useTerminalTabResources', () => { const selectResource = vi.fn() const onResourceEvent = vi.fn() - function render(overrides: Partial = {}) { - const props: HostProps = { + function render(overrides: Partial = {}) { + const props: DesktopTabResourceOptions = { scopeId: SCOPE, resources: [], activeResourceId: null, + selectedResourceId: null, addResource, removeResource, selectResource, @@ -73,7 +65,8 @@ describe('useTerminalTabResources', () => { ...overrides, } act(() => root.render()) - return (next: Partial) => act(() => root.render()) + return (next: Partial) => + act(() => root.render()) } beforeEach(() => { @@ -120,11 +113,15 @@ describe('useTerminalTabResources', () => { { type: 'terminal', id: 'terminal:1', title: 'dir-1' }, { type: 'terminal', id: 'terminal:2', title: 'dir-2' }, ] - const rerender = render({ resources, activeResourceId: 'terminal:1' }) + const rerender = render({ + resources, + activeResourceId: 'terminal:1', + selectedResourceId: 'terminal:1', + }) pushTabs(SCOPE, [shell('1', true), shell('2')], '1') expect(switchTerminal).not.toHaveBeenCalled() - rerender({ activeResourceId: 'terminal:2' }) + rerender({ activeResourceId: 'terminal:2', selectedResourceId: 'terminal:2' }) expect(switchTerminal).toHaveBeenCalledExactlyOnceWith('2', SCOPE, { claim: false }) pushTabs(SCOPE, [shell('1'), shell('2', true)], '2') @@ -137,14 +134,18 @@ describe('useTerminalTabResources', () => { { type: 'terminal', id: 'terminal:2', title: 'dir-2' }, { type: 'file', id: 'f', title: 'notes.md' }, ] - const rerender = render({ resources, activeResourceId: 'terminal:1' }) + const rerender = render({ + resources, + activeResourceId: 'terminal:1', + selectedResourceId: 'terminal:1', + }) pushTabs(SCOPE, [shell('1', true), shell('2')], '1') pushTabs(SCOPE, [shell('1'), shell('2', true)], '2') expect(selectResource).toHaveBeenCalledExactlyOnceWith('terminal:2') selectResource.mockClear() - rerender({ activeResourceId: 'f' }) + rerender({ activeResourceId: 'f', selectedResourceId: 'f' }) pushTabs(SCOPE, [shell('1', true), shell('2')], '1') expect(selectResource).not.toHaveBeenCalled() }) @@ -156,6 +157,7 @@ describe('useTerminalTabResources', () => { { type: 'terminal', id: 'terminal:2', title: 'dir-2' }, ], activeResourceId: 'terminal:1', + selectedResourceId: 'terminal:1', }) pushTabs(SCOPE, [shell('1', true), shell('2')], '1') act(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.ts index e794b0debd7..ae5432d192e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-terminal-tab-resources.ts @@ -1,23 +1,15 @@ import { useMemo } from 'react' import type { TerminalTabState } from '@sim/terminal-protocol' -import type { MothershipResource } from '@/lib/copilot/resources/types' import { terminalIdFromResourceId, terminalResourceId } from '@/lib/terminal/resource-id' import { switchTerminal } from '@/lib/terminal/transport' import { - type DesktopTabResourceCallbacks, + type DesktopTabResourceOptions, useDesktopTabResources, } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources' import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store' const EMPTY_TERMINAL_TABS: TerminalTabState[] = [] -interface UseTerminalTabResourcesOptions extends DesktopTabResourceCallbacks { - /** Desktop terminal scope whose shells back this chat's terminal tabs. */ - scopeId: string - resources: readonly MothershipResource[] - activeResourceId: string | null -} - function showTerminal(resourceId: string, scopeId: string): void { void switchTerminal(terminalIdFromResourceId(resourceId), scopeId, { claim: false }).catch( () => {} @@ -28,15 +20,8 @@ function showTerminal(resourceId: string, scopeId: string): void { * Projects the desktop app's live shells into `terminal` resource tabs, one * per shell. See {@link useDesktopTabResources} for the shared model. */ -export function useTerminalTabResources({ - scopeId, - resources, - activeResourceId, - addResource, - removeResource, - selectResource, - onResourceEvent, -}: UseTerminalTabResourcesOptions): void { +export function useTerminalTabResources(options: DesktopTabResourceOptions): void { + const { scopeId } = options const hasSession = useCopilotTerminalStore((state) => state.sessions[scopeId] !== undefined) const terminalTabs = useCopilotTerminalStore( (state) => state.sessions[scopeId]?.tabs.tabs ?? EMPTY_TERMINAL_TABS @@ -60,18 +45,12 @@ export function useTerminalTabResources({ ) useDesktopTabResources({ + ...options, type: 'terminal', - scopeId, tabs, hasSession, - activeTabId: activeTerminalId && terminalResourceId(activeTerminalId), - agentTabId: agentTerminalId && terminalResourceId(agentTerminalId), + activeTabId: activeTerminalId ? terminalResourceId(activeTerminalId) : null, + agentTabId: agentTerminalId ? terminalResourceId(agentTerminalId) : null, switchTab: showTerminal, - resources, - activeResourceId, - addResource, - removeResource, - selectResource, - onResourceEvent, }) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.test.ts b/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.test.ts index a16456d5add..80435314cf0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.test.ts @@ -1,9 +1,72 @@ import { describe, expect, it } from 'vitest' +import type { MothershipResource } from '@/lib/copilot/resources/types' import { + resolveEffectiveResourceId, resolveResourceEventPresentation, resolveResourceSelectionUpdate, } from '@/app/workspace/[workspaceId]/home/resource-view-policy' +const PAGES: MothershipResource[] = [ + { type: 'browser', id: '1', title: 'Page 1' }, + { type: 'browser', id: '2', title: 'Page 2' }, + { type: 'browser', id: '3', title: 'Page 3' }, +] +const NOTES: MothershipResource = { type: 'file', id: 'notes', title: 'notes.md' } +const SHELLS: MothershipResource[] = [ + { type: 'terminal', id: 'terminal:1', title: 'one' }, + { type: 'terminal', id: 'terminal:2', title: 'two' }, +] +const NO_NATIVE = { browser: null, terminal: null } + +describe('resolveEffectiveResourceId', () => { + it('shows nothing when the strip is empty', () => { + expect(resolveEffectiveResourceId([], null, NO_NATIVE)).toBeNull() + expect(resolveEffectiveResourceId([], 'anything', NO_NATIVE)).toBeNull() + }) + + it('shows the selected resource while it is on screen', () => { + expect(resolveEffectiveResourceId(PAGES, '1', { browser: '3', terminal: null })).toBe('1') + }) + + it('falls back when the selection is no longer on screen', () => { + expect(resolveEffectiveResourceId(PAGES, 'deleted', NO_NATIVE)).toBe('3') + }) + + it('prefers the page the desktop app shows over the last one', () => { + expect(resolveEffectiveResourceId(PAGES, null, { browser: '2', terminal: null })).toBe('2') + }) + + it('prefers the shell the desktop app shows over the last one', () => { + expect( + resolveEffectiveResourceId(SHELLS, null, { browser: null, terminal: 'terminal:1' }) + ).toBe('terminal:1') + }) + + it('keeps the last resource when the desktop app reports nothing', () => { + expect(resolveEffectiveResourceId(PAGES, null, NO_NATIVE)).toBe('3') + expect(resolveEffectiveResourceId(PAGES, null)).toBe('3') + }) + + it('ignores a page the strip no longer holds, such as one just closed', () => { + expect(resolveEffectiveResourceId(PAGES, null, { browser: 'closed', terminal: null })).toBe('3') + }) + + it('ignores the desktop app when the last resource is not one of its tabs', () => { + expect( + resolveEffectiveResourceId([...PAGES, NOTES], null, { browser: '2', terminal: null }) + ).toBe('notes') + }) + + it('does not cross the two desktop kinds', () => { + expect(resolveEffectiveResourceId(PAGES, null, { browser: null, terminal: 'terminal:1' })).toBe( + '3' + ) + expect(resolveEffectiveResourceId(SHELLS, null, { browser: '1', terminal: null })).toBe( + 'terminal:2' + ) + }) +}) + const DEFAULT_INPUT = { activeResourceId: 'file-1', activationRequested: true, diff --git a/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.ts b/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.ts index d163b95259e..378f9ad8947 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/resource-view-policy.ts @@ -1,4 +1,44 @@ import type { SetStateAction } from 'react' +import type { MothershipResource } from '@/lib/copilot/resources/types' + +/** The tab each desktop-backed kind currently shows, as resource ids. */ +export interface NativeActiveTabIds { + browser: string | null + terminal: string | null +} + +/** + * Which resource the panel shows. + * + * An explicit selection wins whenever it is still on screen. Otherwise the + * strip falls back to its last resource — except for the desktop-backed kinds, + * where the desktop app is already showing the tab the user left the chat on. + * Preferring that tab is what makes reopening a chat land where the user left + * it, and deriving it here rather than writing it back means no arrival order + * of tabs, history or native state can leave a tab the user did not pick + * stored as their selection. + */ +export function resolveEffectiveResourceId( + resources: readonly MothershipResource[], + selectedResourceId: string | null, + nativeActiveTabIds?: NativeActiveTabIds +): string | null { + if (resources.length === 0) return null + if (selectedResourceId && resources.some((resource) => resource.id === selectedResourceId)) { + return selectedResourceId + } + const fallback = resources[resources.length - 1] + if (fallback.type === 'browser' || fallback.type === 'terminal') { + const nativeId = nativeActiveTabIds?.[fallback.type] ?? null + if ( + nativeId && + resources.some((resource) => resource.type === fallback.type && resource.id === nativeId) + ) { + return nativeId + } + } + return fallback.id +} export function resolveResourceSelectionUpdate( currentResourceId: string | null, From ad11808e1532b7139b147434836b75c99f3613d7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sun, 13 Sep 2026 00:19:16 -0700 Subject: [PATCH 10/15] fix(sso): require an organization when registering an SSO provider (#7801) --- .../app/api/auth/sso/register/route.test.ts | 104 +++++++-------- apps/sim/app/api/auth/sso/register/route.ts | 118 +++++++----------- apps/sim/ee/sso/hooks/sso.ts | 17 +-- apps/sim/lib/api/contracts/auth.ts | 5 +- apps/sim/lib/auth/auth.ts | 7 +- 5 files changed, 99 insertions(+), 152 deletions(-) diff --git a/apps/sim/app/api/auth/sso/register/route.test.ts b/apps/sim/app/api/auth/sso/register/route.test.ts index b4c4ef1704f..db55de96da8 100644 --- a/apps/sim/app/api/auth/sso/register/route.test.ts +++ b/apps/sim/app/api/auth/sso/register/route.test.ts @@ -88,6 +88,7 @@ const OIDC_BODY = { providerId: 'acme-oidc', issuer: 'https://idp.acme.com', domain: 'acme.com', + orgId: 'org1', clientId: 'client-id', clientSecret: 'client-secret', authorizationEndpoint: 'https://idp.acme.com/authorize', @@ -137,21 +138,21 @@ describe('POST /api/auth/sso/register', () => { it('rejects callers without an Enterprise plan', async () => { mockHasSSOAccess.mockResolvedValue(false) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(403) expect(mockRegisterSSOProvider).not.toHaveBeenCalled() }) it('rejects callers who are not an admin/owner of the target org', async () => { queueMembers([{ organizationId: 'org1', role: 'member' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(403) expect(mockRegisterSSOProvider).not.toHaveBeenCalled() }) it('rejects an invalid domain', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, domain: 'not-a-domain', orgId: 'org1' })) + const res = await POST(request({ ...OIDC_BODY, domain: 'not-a-domain' })) expect(res.status).toBe(400) expect(mockRegisterSSOProvider).not.toHaveBeenCalled() }) @@ -160,7 +161,7 @@ describe('POST /api/auth/sso/register', () => { resetDbChainMock() queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueTableRows(schemaMock.ssoDomain, []) // no verified sso_domain row - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) const json = await res.json() expect(res.status).toBe(403) expect(json.code).toBe('SSO_DOMAIN_NOT_VERIFIED') @@ -172,7 +173,7 @@ describe('POST /api/auth/sso/register', () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueTableRows(schemaMock.ssoDomain, [{ id: 'v' }]) // entry gate: verified queueTableRows(schemaMock.ssoDomain, []) // re-check before write: revoked - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) const json = await res.json() expect(res.status).toBe(403) expect(json.code).toBe('SSO_DOMAIN_NOT_VERIFIED') @@ -185,7 +186,7 @@ describe('POST /api/auth/sso/register', () => { queueTableRows(schemaMock.ssoDomain, [{ id: 'v' }]) // entry gate: verified queueTableRows(schemaMock.ssoDomain, [{ id: 'v' }]) // pre-write re-check: verified queueTableRows(schemaMock.ssoDomain, []) // locking read in the grant: proof gone - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) const json = await res.json() expect(res.status).toBe(403) expect(json.code).toBe('SSO_DOMAIN_NOT_VERIFIED') @@ -221,7 +222,7 @@ describe('POST /api/auth/sso/register', () => { queueProviders([ { domain: 'acme.com', userId: 'u1', organizationId: 'org1', providerId: 'acme-saml' }, ]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) const json = await res.json() expect(res.status).toBe(409) expect(json.code).toBe('SSO_DOMAIN_ALREADY_ROUTED') @@ -238,7 +239,7 @@ describe('POST /api/auth/sso/register', () => { constraint_name: 'sso_provider_org_domain_unique', }) ) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) const json = await res.json() expect(res.status).toBe(409) expect(json.code).toBe('SSO_DOMAIN_ALREADY_ROUTED') @@ -248,14 +249,14 @@ describe('POST /api/auth/sso/register', () => { it('lets the organization add a provider for a different verified domain', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1', domain: 'eng.acme.com' })) + const res = await POST(request({ ...OIDC_BODY, domain: 'eng.acme.com' })) expect(res.status).toBe(200) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) }) it('registers when the domain is unclaimed', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) }) @@ -286,7 +287,7 @@ describe('POST /api/auth/sso/register', () => { it('does not treat the caller’s own provider as a providerId conflict', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([], [{ domain: 'acme.com', userId: 'u1', organizationId: 'org1' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) }) @@ -298,7 +299,7 @@ describe('POST /api/auth/sso/register', () => { */ it('marks the provider domain-verified after registering', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) expect(dbChainMockFns.set).toHaveBeenCalledWith({ domainVerified: true, @@ -311,7 +312,7 @@ describe('POST /api/auth/sso/register', () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([]) queueTableRows(schemaMock.ssoProvider, [{ id: 'p1' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) expect(mockUpdateSSOProvider).toHaveBeenCalledTimes(1) expect(dbChainMockFns.set).toHaveBeenCalledWith({ @@ -346,7 +347,7 @@ describe('POST /api/auth/sso/register', () => { }, ]) // provider already owned → update path - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(403) expect(mockUpdateSSOProvider).toHaveBeenCalledTimes(1) // The conditional grant UPDATE is still issued — it simply matches no rows once @@ -376,7 +377,7 @@ describe('POST /api/auth/sso/register', () => { ]) dbChainMockFns.returning.mockRejectedValueOnce(new Error('trust write failed')) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1', jitProvisioningEnabled: false })) + const res = await POST(request({ ...OIDC_BODY, jitProvisioningEnabled: false })) expect(res.status).toBe(500) expect(mockUpdateSSOProvider).toHaveBeenCalledTimes(1) @@ -397,41 +398,28 @@ describe('POST /api/auth/sso/register', () => { queueTableRows(schemaMock.ssoDomain, [{ id: 'verified-domain' }]) queueTableRows(schemaMock.ssoDomain, [{ id: 'verified-domain' }]) queueTableRows(schemaMock.ssoDomain, []) // locking read in the grant: proof gone - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(403) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) // it was created… expect(dbChainMockFns.delete).toHaveBeenCalled() // …then rolled back }) /** - * A personal provider has no verified domain behind it. On the hosted - * multi-tenant deployment that must grant no linking authority, or anyone able - * to register one could claim a domain they do not own and have their own IdP - * auto-link to existing accounts on it. + * An org-less provider has no `sso_domain` proof behind its domain, and domain + * trust is what auto-links an SSO sign-in into an existing same-email account, + * so the route refuses one before any write. */ - it('does not grant domain trust to a personal provider when hosted', async () => { - setEnvFlags({ isSsoEnabled: true, isHosted: true }) - const res = await POST(request(OIDC_BODY)) - expect(res.status).toBe(200) - expect(dbChainMockFns.set).toHaveBeenCalledWith({ - domainVerified: false, - jitProvisioningEnabled: true, - }) - }) - - it('grants domain trust to a personal provider when self-hosted', async () => { - setEnvFlags({ isSsoEnabled: true, isHosted: false }) - const res = await POST(request(OIDC_BODY)) - expect(res.status).toBe(200) - expect(dbChainMockFns.set).toHaveBeenCalledWith({ - domainVerified: true, - jitProvisioningEnabled: true, - }) + it('refuses a provider without an organization', async () => { + const { orgId: _orgId, ...orgLessBody } = OIDC_BODY + const res = await POST(request(orgLessBody)) + expect(res.status).toBe(400) + expect((await res.json()).error).toContain('Organization ID is required') + expect(mockRegisterSSOProvider).not.toHaveBeenCalled() }) it('persists invite-only provisioning without changing Better Auth provider config', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1', jitProvisioningEnabled: false })) + const res = await POST(request({ ...OIDC_BODY, jitProvisioningEnabled: false })) expect(res.status).toBe(200) expect(dbChainMockFns.set).toHaveBeenCalledWith({ domainVerified: true, @@ -516,9 +504,7 @@ describe('POST /api/auth/sso/register', () => { it('nests the attribute mapping inside oidcConfig (Better Auth reads it there)', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - await POST( - request({ ...OIDC_BODY, orgId: 'org1', mapping: { id: 'oid', email: 'upn', name: 'name' } }) - ) + await POST(request({ ...OIDC_BODY, mapping: { id: 'oid', email: 'upn', name: 'name' } })) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) const sent = mockRegisterSSOProvider.mock.calls[0][0].body expect(sent.mapping).toBeUndefined() // not passed at the top level (silently ignored there) @@ -529,7 +515,7 @@ describe('POST /api/auth/sso/register', () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([]) // no providerId or domain conflicts on either pass queueTableRows(schemaMock.ssoProvider, [{ id: 'p1' }]) // provider already owned → edit - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const body = await res.json() expect(body.message).toContain('updated') @@ -540,15 +526,15 @@ describe('POST /api/auth/sso/register', () => { it('allows the owning tenant to update its own provider for the same domain', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([{ domain: 'acme.com', userId: 'u1', organizationId: 'org1' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) }) - it('lets an org admin adopt their own user-scoped provider for the same domain', async () => { + it("does not report the caller's own org-less provider as another tenant's claim", async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([{ domain: 'acme.com', userId: 'u1', organizationId: null }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) }) @@ -556,14 +542,14 @@ describe('POST /api/auth/sso/register', () => { it("still blocks an org admin from claiming another user's user-scoped domain", async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) queueProviders([{ domain: 'acme.com', userId: 'someone-else', organizationId: null }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(409) expect(mockRegisterSSOProvider).not.toHaveBeenCalled() }) it('normalizes the domain before persisting it', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, domain: 'ACME.com', orgId: 'org1' })) + const res = await POST(request({ ...OIDC_BODY, domain: 'ACME.com' })) expect(res.status).toBe(200) expect(mockRegisterSSOProvider).toHaveBeenCalledTimes(1) const config = mockRegisterSSOProvider.mock.calls[0][0].body @@ -572,7 +558,7 @@ describe('POST /api/auth/sso/register', () => { it('passes skipDiscovery since Sim already resolved and validated the OIDC endpoints', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.skipDiscovery).toBe(true) @@ -580,7 +566,7 @@ describe('POST /api/auth/sso/register', () => { it('omits userInfoEndpoint when skipUserInfoEndpoint is requested, forcing ID token claims', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, skipUserInfoEndpoint: true, orgId: 'org1' })) + const res = await POST(request({ ...OIDC_BODY, skipUserInfoEndpoint: true })) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.userInfoEndpoint).toBeUndefined() @@ -594,7 +580,7 @@ describe('POST /api/auth/sso/register', () => { } return { isValid: true, resolvedIP: '1.2.3.4' } }) - const res = await POST(request({ ...OIDC_BODY, skipUserInfoEndpoint: true, orgId: 'org1' })) + const res = await POST(request({ ...OIDC_BODY, skipUserInfoEndpoint: true })) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.userInfoEndpoint).toBeUndefined() @@ -624,7 +610,7 @@ describe('POST /api/auth/sso/register', () => { jwksEndpoint: undefined, skipUserInfoEndpoint: true, } - const res = await POST(request({ ...discoveredBody, orgId: 'org1' })) + const res = await POST(request(discoveredBody)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.userInfoEndpoint).toBeUndefined() @@ -632,7 +618,7 @@ describe('POST /api/auth/sso/register', () => { it('keeps userInfoEndpoint when skipUserInfoEndpoint is not requested', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.userInfoEndpoint).toBe('https://idp.acme.com/userinfo') @@ -656,7 +642,7 @@ describe('POST /api/auth/sso/register', () => { tokenEndpoint: undefined, jwksEndpoint: undefined, } - const res = await POST(request({ ...discoveredBody, orgId: 'org1' })) + const res = await POST(request(discoveredBody)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.tokenEndpointAuthentication).toBe('client_secret_post') @@ -670,7 +656,7 @@ describe('POST /api/auth/sso/register', () => { token_endpoint_auth_methods_supported: ['client_secret_post'], }), }) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.tokenEndpointAuthentication).toBe('client_secret_post') @@ -680,7 +666,7 @@ describe('POST /api/auth/sso/register', () => { it('registers successfully when discovery is unreachable and all endpoints are explicit', async () => { queueMembers([{ organizationId: 'org1', role: 'owner' }]) mockSecureFetchWithPinnedIP.mockRejectedValue(new Error('ECONNREFUSED')) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.skipDiscovery).toBe(true) @@ -696,7 +682,7 @@ describe('POST /api/auth/sso/register', () => { token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], }), }) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.tokenEndpointAuthentication).toBe('client_secret_post') @@ -708,7 +694,7 @@ describe('POST /api/auth/sso/register', () => { ok: true, json: async () => ({}), }) - const res = await POST(request({ ...OIDC_BODY, orgId: 'org1' })) + const res = await POST(request(OIDC_BODY)) expect(res.status).toBe(200) const config = mockRegisterSSOProvider.mock.calls[0][0].body expect(config.oidcConfig.tokenEndpointAuthentication).toBe('client_secret_post') @@ -728,7 +714,7 @@ describe('POST /api/auth/sso/register', () => { tokenEndpoint: undefined, jwksEndpoint: undefined, } - const res = await POST(request({ ...discoveredBody, orgId: 'org1' })) + const res = await POST(request(discoveredBody)) const json = await res.json() expect(res.status).toBe(400) expect(json.error).toContain('resolves to a private IP address') diff --git a/apps/sim/app/api/auth/sso/register/route.ts b/apps/sim/app/api/auth/sso/register/route.ts index c4991dc4271..ff657118256 100644 --- a/apps/sim/app/api/auth/sso/register/route.ts +++ b/apps/sim/app/api/auth/sso/register/route.ts @@ -2,13 +2,13 @@ import { db, member, ssoDomain, ssoProvider } from '@sim/db' import { createLogger } from '@sim/logger' import { getErrorMessage, getPostgresConstraintName } from '@sim/utils/errors' import { normalizeSSODomain } from '@sim/utils/sso-domain' -import { and, eq, isNull, sql } from 'drizzle-orm' +import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { ssoRegistrationContract } from '@/lib/api/contracts/auth' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { auth, getSession } from '@/lib/auth' import { hasSSOAccess } from '@/lib/billing' -import { isHosted, isSsoEnabled } from '@/lib/core/config/env-flags' +import { isSsoEnabled } from '@/lib/core/config/env-flags' import { secureFetchWithPinnedIP, validateUrlWithDNS, @@ -116,18 +116,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const body = parsed.data.body const { providerId, issuer, providerType, mapping, orgId, jitProvisioningEnabled } = body - if (orgId) { - const [membership] = await db - .select({ organizationId: member.organizationId, role: member.role }) - .from(member) - .where(and(eq(member.userId, session.user.id), eq(member.organizationId, orgId))) - .limit(1) - if (!membership) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - if (membership.role !== 'owner' && membership.role !== 'admin') { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } + /** + * Always org-scoped: an org-less provider has no `sso_domain` proof, so only + * operators create one, via `packages/db/scripts/register-sso-provider.ts`. + */ + const [membership] = await db + .select({ organizationId: member.organizationId, role: member.role }) + .from(member) + .where(and(eq(member.userId, session.user.id), eq(member.organizationId, orgId))) + .limit(1) + if (!membership) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + if (membership.role !== 'owner' && membership.role !== 'admin') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } const domain = normalizeSSODomain(body.domain) @@ -142,20 +144,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { /** * Configuring org SSO for a domain requires DNS-proven ownership; without it * a first-come claim lets any org wire another company's domain to their own - * IdP. Migration 0266 grandfathered existing domains. Org-less SSO is not gated. + * IdP. Migration 0266 grandfathered existing domains. */ + const verifiedDomainClause = and( + eq(ssoDomain.organizationId, orgId), + eq(ssoDomain.domain, domain), + eq(ssoDomain.status, 'verified') + ) + const isOrgDomainVerified = async (): Promise => { - if (!orgId) return true const [verified] = await db .select({ id: ssoDomain.id }) .from(ssoDomain) - .where( - and( - eq(ssoDomain.organizationId, orgId), - eq(ssoDomain.domain, domain), - eq(ssoDomain.status, 'verified') - ) - ) + .where(verifiedDomainClause) .limit(1) return Boolean(verified) } @@ -173,13 +174,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { // window where the proof is removed while discovery is in flight. if (!(await isOrgDomainVerified())) return domainNotVerifiedResponse() + /** + * An org-less provider the caller created counts as theirs, so its claim on + * a domain is not reported as another tenant's. + */ const isOwnedByCaller = (provider: { userId: string | null organizationId: string | null - }): boolean => { - if (provider.userId === session.user.id && !provider.organizationId) return true - return orgId ? provider.organizationId === orgId : false - } + }): boolean => + provider.organizationId === orgId || + (provider.userId === session.user.id && !provider.organizationId) + + const ownerClause = and( + eq(ssoProvider.providerId, providerId), + eq(ssoProvider.organizationId, orgId) + ) /** * Refuses the domain when another tenant has claimed it, or when the caller @@ -236,10 +245,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const findProviderIdConflict = async () => ( await db - .select({ - userId: ssoProvider.userId, - organizationId: ssoProvider.organizationId, - }) + .select({ userId: ssoProvider.userId, organizationId: ssoProvider.organizationId }) .from(ssoProvider) .where(eq(ssoProvider.providerId, providerId)) ).find((provider) => !isOwnedByCaller(provider)) @@ -274,7 +280,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { providerId, issuer, domain, - ...(orgId ? { organizationId: orgId } : {}), + organizationId: orgId, } if (providerType === 'oidc') { @@ -292,13 +298,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { let clientSecret = rawClientSecret if (rawClientSecret === REDACTED_MARKER) { - const ownerClause = orgId - ? and(eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, orgId)) - : and( - eq(ssoProvider.providerId, providerId), - eq(ssoProvider.userId, session.user.id), - isNull(ssoProvider.organizationId) - ) const [existing] = await db .select({ oidcConfig: ssoProvider.oidcConfig }) .from(ssoProvider) @@ -632,18 +631,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { // edit through updateSSOProvider so re-saving an SSO config works instead of // failing. The verification gate above already ran against the target domain, // so an edit that moves SSO to an unverified domain is still blocked. - // The personal branch MUST require a null org: org providers store - // userId = their creator, so without it an org admin could send a - // personal-mode request (which skips the membership check and the - // verification gate) yet still match — and then update — their org's - // provider, moving it to an unverified domain. Mirrors isOwnedByCaller. - const ownerClause = orgId - ? and(eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, orgId)) - : and( - eq(ssoProvider.providerId, providerId), - eq(ssoProvider.userId, session.user.id), - isNull(ssoProvider.organizationId) - ) // Config columns are captured, not just the id: an update whose trust grant is // refused has to be undone, or the rejected config stays stored and goes live // the moment the domain is verified again. @@ -668,29 +655,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { * UPDATE waits can still grant trust after ownership is gone. `FOR SHARE` * orders the two — the delete blocks until this commits, and if it committed * first the SELECT finds nothing. - * - * Org-less SSO is self-host-only (Sim's UI always registers org-scoped) and - * has no proof behind it, so it is trusted only when self-hosted. */ - const grantProviderDomainTrust = async (): Promise => { - if (!orgId) { - await db - .update(ssoProvider) - .set({ domainVerified: !isHosted, jitProvisioningEnabled }) - .where(ownerClause) - return true - } - return db.transaction(async (tx) => { + const grantProviderDomainTrust = (): Promise => + db.transaction(async (tx) => { const [proof] = await tx .select({ id: ssoDomain.id }) .from(ssoDomain) - .where( - and( - eq(ssoDomain.organizationId, orgId), - eq(ssoDomain.domain, domain), - eq(ssoDomain.status, 'verified') - ) - ) + .where(verifiedDomainClause) .limit(1) .for('share') if (!proof) return false @@ -702,7 +673,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { .returning({ id: ssoProvider.id }) return granted.length > 0 }) - } if (existingOwnedProvider) { const revertProviderUpdate = async (): Promise => { @@ -784,12 +754,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { // registerSSOProvider spreads the created row's `id` at runtime, but the // typed return omits it — read it defensively and only delete when it's a // real id, so a future shape change can't turn the rollback into a silent - // no-op that leaves a provider on an unverified domain. `orgId` is checked - // only to narrow it: the org-less path grants unconditionally, so a refused - // grant always means an org-scoped registration. + // no-op that leaves a provider on an unverified domain. // double-cast-allowed: Better Auth's return type omits the runtime `id` const createdRowId = (registration as unknown as { id?: unknown }).id - if (orgId && typeof createdRowId === 'string' && createdRowId.length > 0) { + if (typeof createdRowId === 'string' && createdRowId.length > 0) { await db .delete(ssoProvider) .where(and(eq(ssoProvider.id, createdRowId), eq(ssoProvider.organizationId, orgId))) diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts index 28d399e5256..aa854c21b30 100644 --- a/apps/sim/ee/sso/hooks/sso.ts +++ b/apps/sim/ee/sso/hooks/sso.ts @@ -52,27 +52,18 @@ export function useSSOProviders({ enabled = true, organizationId }: UseSSOProvid /** * Configure SSO provider mutation */ -type ConfigureSSOParams = Record - export function useConfigureSSO() { const queryClient = useQueryClient() return useMutation({ - mutationFn: (config: ConfigureSSOParams) => - requestJson(ssoRegistrationContract, { - body: config as SsoRegistrationBody, - }), + mutationFn: (config: SsoRegistrationBody) => + requestJson(ssoRegistrationContract, { body: config }), onSettled: (_data, _error, variables) => { - const orgId = typeof variables.orgId === 'string' ? variables.orgId : undefined /** Awaited, so the caller navigates against a list that already holds the change. */ return Promise.all([ queryClient.invalidateQueries({ queryKey: ssoKeys.providers() }), - ...(orgId - ? [ - queryClient.invalidateQueries({ queryKey: organizationKeys.detail(orgId) }), - queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }), - ] - : []), + queryClient.invalidateQueries({ queryKey: organizationKeys.detail(variables.orgId) }), + queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }), ]) }, }) diff --git a/apps/sim/lib/api/contracts/auth.ts b/apps/sim/lib/api/contracts/auth.ts index 247df754b9a..2acb8998829 100644 --- a/apps/sim/lib/api/contracts/auth.ts +++ b/apps/sim/lib/api/contracts/auth.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { organizationIdSchema } from '@/lib/api/contracts/primitives' import type { ContractJsonResponse } from '@/lib/api/contracts/types' import { defineRouteContract } from '@/lib/api/contracts/types' @@ -33,7 +34,7 @@ export const ssoRegistrationBodySchema = z.discriminatedUnion('providerType', [ providerId: z.string().min(1, 'Provider ID is required'), issuer: z.string().url('Issuer must be a valid URL'), domain: z.string().min(1, 'Domain is required'), - orgId: z.string().optional(), + orgId: organizationIdSchema, jitProvisioningEnabled: z.boolean().default(true), mapping: ssoMappingSchema, clientId: z.string().min(1, 'Client ID is required for OIDC'), @@ -61,7 +62,7 @@ export const ssoRegistrationBodySchema = z.discriminatedUnion('providerType', [ providerId: z.string().min(1, 'Provider ID is required'), issuer: z.string().url('Issuer must be a valid URL'), domain: z.string().min(1, 'Domain is required'), - orgId: z.string().optional(), + orgId: organizationIdSchema, jitProvisioningEnabled: z.boolean().default(true), mapping: ssoMappingSchema, entryPoint: z.string().url('Entry point must be a valid URL for SAML'), diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index ace5b81ed3a..f5c839f6c8e 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1350,9 +1350,10 @@ export const auth = betterAuth({ * `email_verified` claim substitutes for the domain binding * entirely: an IdP could assert any address — including one from a * domain it does not own — and auto-link into that user's existing - * account. Since a provider row can be registered by any Enterprise - * org admin (and by any signed-in user when self-hosted), trusting - * the claim makes every account reachable from any tenant's IdP. + * account. Since a provider row can be registered by any + * organization owner or admin (or by an operator via the register + * script), trusting the claim makes every account reachable from + * any tenant's IdP. * * Turning it on only ever set `emailVerified` on the local row; it * was never what made linking work. Entra omits the claim, and SAML From f75f55afeb7c41ab40f1af398eb9b3221cfdbd70 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sun, 13 Sep 2026 00:19:46 -0700 Subject: [PATCH 11/15] fix(mcp): always guard the MCP transport and validate on every managed connect (#7802) McpClient no longer falls back to the global fetch when no validated address is supplied; it always uses the SSRF-guarded transport, pinning only a validated private address. McpConnectionManager.connect validates the destination itself on every dial, reconnects included, instead of trusting a caller-supplied address. --- apps/sim/lib/mcp/client.test.ts | 34 +++++++++ apps/sim/lib/mcp/client.ts | 23 +++--- apps/sim/lib/mcp/connection-manager.test.ts | 85 +++++++++++++++++++++ apps/sim/lib/mcp/connection-manager.ts | 15 +++- apps/sim/lib/mcp/service.ts | 4 +- apps/sim/lib/mcp/types.ts | 10 +-- 6 files changed, 150 insertions(+), 21 deletions(-) diff --git a/apps/sim/lib/mcp/client.test.ts b/apps/sim/lib/mcp/client.test.ts index 1d5ff39964d..bb7784bdbed 100644 --- a/apps/sim/lib/mcp/client.test.ts +++ b/apps/sim/lib/mcp/client.test.ts @@ -22,6 +22,7 @@ vi.mock('@sim/logger', () => ({ vi.mock('@/lib/mcp/pinned-fetch', () => ({ createGuardedMcpFetch: vi.fn(() => ({ fetch: vi.fn(), close: mockPinnedClose })), + createPinnedPrivateMcpFetch: vi.fn(() => ({ fetch: vi.fn(), close: mockPinnedClose })), })) /** @@ -72,6 +73,7 @@ vi.mock('@/lib/core/execution-limits', () => ({ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { McpClient } from '@/lib/mcp/client' +import { createGuardedMcpFetch, createPinnedPrivateMcpFetch } from '@/lib/mcp/pinned-fetch' import { type McpClientOptions, McpOauthAuthorizationRequiredError, @@ -372,6 +374,38 @@ describe('McpClient notification handler', () => { expect(JSON.stringify(mockLogger.error.mock.calls)).not.toContain(secret) }) + it('keeps the transport on the SSRF guard when no validated address is supplied', () => { + new McpClient({ + config: createConfig(), + securityPolicy: { requireConsent: false, auditLevel: 'basic' }, + }) + + const guarded = vi.mocked(createGuardedMcpFetch).mock.results.at(-1)?.value + expect(createGuardedMcpFetch).toHaveBeenCalledWith('https://test.example.com/mcp') + expect(createPinnedPrivateMcpFetch).not.toHaveBeenCalled() + expect(vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1)?.[1]?.fetch).toBe( + guarded.fetch + ) + }) + + it('pins the transport to a validated private address', () => { + new McpClient({ + config: createConfig(), + securityPolicy: { requireConsent: false, auditLevel: 'basic' }, + resolvedIP: '10.0.0.5', + }) + + const pinned = vi.mocked(createPinnedPrivateMcpFetch).mock.results.at(-1)?.value + expect(createPinnedPrivateMcpFetch).toHaveBeenCalledWith( + '10.0.0.5', + 'https://test.example.com/mcp' + ) + expect(createGuardedMcpFetch).not.toHaveBeenCalled() + expect(vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1)?.[1]?.fetch).toBe( + pinned.fetch + ) + }) + it('closes the pinned transport Agent when connect fails', async () => { mockSdkConnect.mockRejectedValueOnce(new Error('connect boom')) const client = new McpClient({ diff --git a/apps/sim/lib/mcp/client.ts b/apps/sim/lib/mcp/client.ts index 296478e16cb..bcfe4a5072d 100644 --- a/apps/sim/lib/mcp/client.ts +++ b/apps/sim/lib/mcp/client.ts @@ -119,19 +119,18 @@ export class McpClient { throw new McpError('OAuth MCP server must use one authentication strategy') } const useOauth = this.config.authType === 'oauth' - // `resolvedIP` is null only when the hostname still carries an unresolved env-var - // reference, which is checked again once it resolves. Otherwise the guard validates - // addresses per-connect. A private/loopback resolvedIP only reaches here on a - // self-hosted deployment whose policy permits it, and that case pins to the address - // that was validated rather than to whatever the name resolves to next. - const guarded = resolvedIP - ? isPrivateIp(resolvedIP) + // The transport never runs on the global fetch: the guard validates addresses + // per-connect and redirects per-hop whether or not a caller validated the URL + // first. A private/loopback resolvedIP only reaches here on a self-hosted + // deployment whose policy permits it, and that case pins to the address that + // was validated rather than to whatever the name resolves to next. + const guarded = + resolvedIP && isPrivateIp(resolvedIP) ? createPinnedPrivateMcpFetch(resolvedIP, this.config.url) : createGuardedMcpFetch(this.config.url) - : undefined - this.closeGuardedTransport = guarded?.close + this.closeGuardedTransport = guarded.close const oauthFetch = useOauth - ? createMcpEndpointFetch(guarded?.fetch ?? fetch, { + ? createMcpEndpointFetch(guarded.fetch, { serverUrl: this.config.url, headers: this.config.headers, }) @@ -142,11 +141,11 @@ export class McpClient { serverUrl: this.config.url, fetch: oauthFetch, }) - : (oauthFetch ?? guarded?.fetch) + : (oauthFetch ?? guarded.fetch) this.transport = new StreamableHTTPClientTransport(new URL(this.config.url), { authProvider: useOauth ? this.authProvider : undefined, ...(useOauth ? {} : { requestInit: { headers: this.config.headers } }), - ...(transportFetch ? { fetch: transportFetch } : {}), + fetch: transportFetch, }) this.client = new Client( diff --git a/apps/sim/lib/mcp/connection-manager.test.ts b/apps/sim/lib/mcp/connection-manager.test.ts index 6d66ca07340..37eea450396 100644 --- a/apps/sim/lib/mcp/connection-manager.test.ts +++ b/apps/sim/lib/mcp/connection-manager.test.ts @@ -34,8 +34,12 @@ const { mockOnToolsChanged, mockPublishToolsChanged, mockGetOrCreateOauthRow, + mockValidateMcpDomain, + mockValidateMcpServerSsrf, } = vi.hoisted(() => ({ MockMcpClientConstructor: vi.fn(), + mockValidateMcpDomain: vi.fn(), + mockValidateMcpServerSsrf: vi.fn(), mockOnToolsChanged: vi.fn(() => vi.fn()), mockPublishToolsChanged: vi.fn(), mockGetOrCreateOauthRow: vi.fn(), @@ -50,6 +54,10 @@ vi.mock('@/lib/mcp/pubsub', () => ({ vi.mock('@/lib/mcp/client', () => ({ McpClient: MockMcpClientConstructor, })) +vi.mock('@/lib/mcp/domain-check', () => ({ + validateMcpDomain: mockValidateMcpDomain, + validateMcpServerSsrf: mockValidateMcpServerSsrf, +})) vi.mock('@/lib/mcp/oauth', () => ({ getOrCreateOauthRow: mockGetOrCreateOauthRow, loadPreregisteredClient: vi.fn(), @@ -76,6 +84,7 @@ describe('McpConnectionManager', () => { beforeEach(() => { vi.clearAllMocks() + mockValidateMcpServerSsrf.mockResolvedValue('93.184.216.34') mockGetOrCreateOauthRow.mockResolvedValue({ id: 'oauth-row-1', mcpServerId: 'server-oauth', @@ -277,6 +286,82 @@ describe('McpConnectionManager', () => { }) }) + describe('destination validation', () => { + function mockClients(closeHandlers: Array<() => void> = []): MockMcpClient[] { + const instances: MockMcpClient[] = [] + MockMcpClientConstructor.mockImplementation( + class { + constructor() { + const instance: MockMcpClient = { + connect: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), + hasListChangedCapability: vi.fn().mockReturnValue(true), + onClose: vi.fn().mockImplementation((handler: () => void) => { + closeHandlers.push(handler) + }), + } + instances.push(instance) + Object.assign(this, instance) + } + } + ) + return instances + } + + it('validates the destination and hands the resolved address to the client', async () => { + mockClients() + const mgr = createFreshManager() + const config = serverConfig('server-validate') + + await mgr.connect(config, 'user-1', 'ws-1') + + expect(mockValidateMcpDomain).toHaveBeenCalledWith(config.url) + expect(mockValidateMcpServerSsrf).toHaveBeenCalledWith(config.url) + const options: McpClientOptions = MockMcpClientConstructor.mock.calls[0][0] + expect(options.resolvedIP).toBe('93.184.216.34') + }) + + it('never constructs a client for a refused destination and releases the connecting slot', async () => { + const instances = mockClients() + mockValidateMcpServerSsrf.mockRejectedValueOnce(new Error('refused by egress policy')) + const mgr = createFreshManager() + const config = serverConfig('server-refused') + + await expect(mgr.connect(config, 'user-1', 'ws-1')).rejects.toThrow( + 'refused by egress policy' + ) + expect(instances).toHaveLength(0) + + const retry = await mgr.connect(config, 'user-1', 'ws-1') + expect(retry.supportsListChanged).toBe(true) + expect(instances).toHaveLength(1) + }) + + it('re-validates the destination before every reconnect', async () => { + vi.useFakeTimers() + const closeHandlers: Array<() => void> = [] + const instances = mockClients(closeHandlers) + const mgr = createFreshManager() + const config = serverConfig('server-reconnect') + + await mgr.connect(config, 'user-1', 'ws-1') + mockValidateMcpServerSsrf.mockRejectedValueOnce(new Error('refused by egress policy')) + mockValidateMcpServerSsrf.mockResolvedValueOnce('93.184.216.35') + + closeHandlers[0]() + await vi.advanceTimersByTimeAsync(2_000) + expect(mockValidateMcpServerSsrf).toHaveBeenCalledTimes(2) + expect(instances).toHaveLength(1) + + await vi.advanceTimersByTimeAsync(5_000) + expect(mockValidateMcpServerSsrf).toHaveBeenCalledTimes(3) + expect(instances).toHaveLength(2) + const options: McpClientOptions = MockMcpClientConstructor.mock.calls[1][0] + expect(options.resolvedIP).toBe('93.184.216.35') + expect(mgr.hasConnection('server-reconnect')).toBe(true) + }) + }) + describe('dispose', () => { it('rejects new connections after dispose', async () => { MockMcpClientConstructor.mockImplementation( diff --git a/apps/sim/lib/mcp/connection-manager.ts b/apps/sim/lib/mcp/connection-manager.ts index a0e6d448bd2..ebaa67f96d0 100644 --- a/apps/sim/lib/mcp/connection-manager.ts +++ b/apps/sim/lib/mcp/connection-manager.ts @@ -14,6 +14,7 @@ import { createLogger } from '@sim/logger' import { backoffWithJitter } from '@sim/utils/retry' import { isTest } from '@/lib/core/config/env-flags' import { McpClient } from '@/lib/mcp/client' +import { validateMcpDomain, validateMcpServerSsrf } from '@/lib/mcp/domain-check' import { getOrCreateOauthRow, loadPreregisteredClient, SimMcpOauthProvider } from '@/lib/mcp/oauth' import { mcpPubSub } from '@/lib/mcp/pubsub' import { @@ -106,12 +107,19 @@ export class McpConnectionManager { * * If the server does NOT support `listChanged`, the client is disconnected * immediately — there's nothing to listen for. + * + * `config` must already have its env-var references resolved. The destination + * is validated here on every dial, including reconnects, so the address is + * never carried over from an earlier resolution. + * + * @throws McpDomainNotAllowedError when the domain is not allowlisted + * @throws McpSsrfError when the egress policy refuses the destination + * @throws McpDnsResolutionError when the hostname cannot be resolved */ async connect( config: McpServerConfig, userId: string, - workspaceId: string, - resolvedIP?: string | null + workspaceId: string ): Promise<{ supportsListChanged: boolean }> { if (this.disposed) { logger.warn('Connection manager is disposed, ignoring connect request') @@ -172,6 +180,9 @@ export class McpConnectionManager { } } + validateMcpDomain(config.url) + const resolvedIP = await validateMcpServerSsrf(config.url) + const client = new McpClient({ config, securityPolicy: { diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index 74ce71f6d66..7d93b58112b 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -1095,12 +1095,12 @@ class McpService { // survives a transport loss and would block that fresh reconnect. void (async () => { try { - const { config: resolvedConfig, resolvedIP } = await this.resolveConfigEnvVars( + const { config: resolvedConfig } = await resolveMcpConfigEnvVars( config, userId, workspaceId ) - await manager.connect(resolvedConfig, userId, workspaceId, resolvedIP) + await manager.connect(resolvedConfig, userId, workspaceId) } catch (err) { logger.warn(`[${requestId}] Persistent connection failed for ${config.name}:`, err) } diff --git a/apps/sim/lib/mcp/types.ts b/apps/sim/lib/mcp/types.ts index 91dbfadde3c..a4007638a03 100644 --- a/apps/sim/lib/mcp/types.ts +++ b/apps/sim/lib/mcp/types.ts @@ -197,11 +197,11 @@ export interface McpClientOptions { securityPolicy?: McpSecurityPolicy onToolsChanged?: McpToolsChangedCallback /** - * Pre-resolved IP address to pin all transport HTTP connections to. When - * set, the SDK transport uses a custom fetch backed by an undici Agent with - * a fixed DNS lookup, preventing DNS-rebinding (TOCTOU) attacks between - * URL validation and connection. Should be supplied by callers that have - * just validated the URL via `validateMcpServerSsrf`. + * Address returned by `validateMcpServerSsrf` for this URL. A private/loopback + * address (only permitted on a self-hosted deployment whose policy allows it) + * pins every transport connection to it, so the name cannot rebind elsewhere + * after validation. A public address or none leaves the transport on the SSRF + * guard, which validates every connect and redirect hop itself. */ resolvedIP?: string /** From ae32a4dc228f56ebd27c62db15f73ed325b1dfe8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sun, 13 Sep 2026 00:20:09 -0700 Subject: [PATCH 12/15] feat(copilot): show model-authored tool activity (#7803) * feat(copilot): show model-authored tool activity * chore(copilot): remove unused outcome title wrappers * fix(copilot): keep model activity outcomes authoritative --- .../hero-chat-loop/hero-tool-call-item.tsx | 3 +- .../agent-group/agent-group-view.tsx | 9 +- .../agent-group/agent-group.test.ts | 39 ++++++- .../agent-group/tool-activity-group.tsx | 7 +- .../agent-group/tool-call-item.test.tsx | 84 +++++++++++++++ .../components/agent-group/tool-call-item.tsx | 11 +- .../message-content/message-content.test.ts | 27 +++++ .../message-content/message-content.tsx | 10 +- .../hooks/stream/turn-model-serialize.test.ts | 101 ++++++++++++++++++ .../home/hooks/stream/turn-model-serialize.ts | 7 +- .../home/hooks/stream/turn-model.ts | 10 +- .../[workspaceId]/home/hooks/use-chat.ts | 6 ++ .../app/workspace/[workspaceId]/home/types.ts | 3 + apps/sim/lib/api/contracts/copilot.ts | 1 + apps/sim/lib/copilot/chat/display-message.ts | 3 + .../copilot/chat/effective-transcript.test.ts | 64 +++++++++++ .../lib/copilot/chat/effective-transcript.ts | 24 +++-- .../copilot/chat/persisted-message.test.ts | 81 ++++++++++++++ .../sim/lib/copilot/chat/persisted-message.ts | 11 ++ .../generated/mothership-stream-v1-schema.ts | 4 + .../copilot/generated/mothership-stream-v1.ts | 1 + .../copilot/request/handlers/handlers.test.ts | 85 +++++++++++++++ apps/sim/lib/copilot/request/handlers/tool.ts | 39 +++++-- apps/sim/lib/copilot/request/types.ts | 2 + .../lib/copilot/tools/tool-display.test.ts | 49 +++++++++ apps/sim/lib/copilot/tools/tool-display.ts | 71 ++++++------ 26 files changed, 697 insertions(+), 55 deletions(-) diff --git a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx index b659eae8b45..f32b4385062 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx @@ -11,6 +11,7 @@ export function HeroToolCallItem({ renderStatus, toolName, displayTitle, + activityDescription, status, }: ToolCallItemProps) { const Icon = @@ -21,7 +22,7 @@ export function HeroToolCallItem({ : getToolIcon(toolName) const activity = ( } /> diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx index c9733459807..32183aec931 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx @@ -4,6 +4,7 @@ import { type ComponentType, type ReactNode, useMemo, useState } from 'react' import { ActivityStatus } from '@/components/ui/activity-status' import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport' import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools' +import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display' import { ActivityDisclosure } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure' import { BrowserAgentIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon' import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown' @@ -52,7 +53,12 @@ export interface AgentGroupProps { } function toolStatusTitle(tool: ToolCallData): string { - return tool.displayTitle || String(tool.toolName ?? '') + return getToolStatusDisplayTitle( + tool.displayTitle || String(tool.toolName ?? ''), + tool.status, + tool.toolName, + tool.activityDescription + ) } /** @@ -213,6 +219,7 @@ export function AgentGroupView({ toolCallId={item.data.id} toolName={item.data.toolName} displayTitle={item.data.displayTitle} + activityDescription={item.data.activityDescription} status={item.data.status} params={item.data.params} result={item.data.result} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts index 8dd4c725517..1e0b1923cdc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts @@ -120,6 +120,43 @@ describe('AgentGroup inline main activity', () => { container.remove() }) + it.each(['mothership', 'workflow', 'browser'])( + 'uses the same model description in the %s live header and expanded row', + (agentName) => { + act(() => + root.render( + createElement(AgentGroup, { + agentName, + agentLabel: agentName, + defaultExpanded: true, + isLaneOpen: true, + isStreaming: true, + items: [ + { + type: 'tool', + data: { + id: 'described-read', + toolName: 'read', + displayTitle: 'Reading files', + activityDescription: 'Checking the project timeline', + status: 'executing', + }, + }, + ], + }) + ) + ) + + const statuses = [...container.querySelectorAll('[role="status"]')] + expect(statuses).toHaveLength(agentName === 'mothership' ? 1 : 2) + for (const status of statuses) { + expect(status.textContent).toContain('Checking the project timeline') + } + expect(container.textContent).not.toContain('Reading files') + expect(container.querySelector('[class*="shimmer"]')).not.toBeNull() + } + ) + it.each([ ['executing', 'Reading notes'], ['success', 'Read notes'], @@ -733,6 +770,6 @@ describe('AgentGroup nested status line', () => { namedTool('Reading workflow', 'success' as ToolCallStatus, 1), group([namedTool('Deploying Invoice Sync as API', 'success' as ToolCallStatus, 2)]), ]) - expect(header).toContain('Workflow Agent — Deploying Invoice Sync as API') + expect(header).toContain('Workflow Agent — Deployed Invoice Sync as API') }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx index d426fe587da..d86c2fbd3af 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx @@ -15,7 +15,12 @@ const MAX_SUMMARY_ACTIONS = 2 export function getToolActivitySummary(tools: ToolCallData[]): string { if (tools.length === 1) { const tool = tools[0] - return getToolStatusDisplayTitle(tool.displayTitle, tool.status, tool.toolName) + return getToolStatusDisplayTitle( + tool.displayTitle, + tool.status, + tool.toolName, + tool.activityDescription + ) } const labels = new Set() let failed = 0 diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx index b52d7b1b0e6..52e9489811d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx @@ -48,6 +48,90 @@ describe('ToolCallItem', () => { expect(markup).not.toContain('Writing brief.md') }) + it.each([ + ['executing', 'Checking the invoice totals'], + ['success', 'Checked the invoice totals'], + ['error', 'Failed checking the invoice totals'], + ['cancelled', 'Stopped checking the invoice totals'], + ['rejected', 'Failed checking the invoice totals'], + ['skipped', 'Skipped checking the invoice totals'], + ] as const)( + 'projects %s from the actual tool status onto the model description', + (status, title) => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain(title) + expect(markup).not.toContain('report.md') + } + ) + + it.each([' ', 'a'.repeat(161)])( + 'uses the existing title for an invalid description', + (activityDescription) => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Searching files') + } + ) + + it('keeps an executing wait countdown in place of the model phrase', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('10s') + expect(markup).not.toContain('Waiting for the export') + }) + + it('renders model descriptions as text, without interpreting markup', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('<script>') + expect(markup).not.toContain('