From f0311a6f5ee249452ad0c62c5a92b109fd68a97e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 20 May 2026 02:44:09 -0700 Subject: [PATCH 01/16] feat(table): chunked dispatcher + workflow cascade (#4672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(table): chunked dispatcher for workflow-column runs Replaces the all-rows-at-once runWorkflowColumn with a row-window dispatcher backed by a new table_run_dispatches row. Each user click inserts a dispatch row and triggers a trigger.dev task that crawls the table 20 rows at a time, re-enqueueing itself between windows. The HTTP/Mothership entrypoints return { dispatchId } immediately instead of holding the request open for minutes on multi-thousand-row dispatches. - Per-row cancel stamps cancelledAt; the dispatcher skips cells whose cancelledAt > dispatch.requestedAt so a mid-cascade cancel sticks even under isManualRun. - Table-wide cancel marks active dispatches cancelled atomically so the dispatcher bails on its next iteration. - New 'dispatch' SSE event variant plumbed; client ignores for v1. * fix(table): eager bulk clear on column run so cells flip immediately Run-column with run-mode 'all' wasn't visually flipping rows that already had data — the cell renderer's "value wins" branch kept showing the prior output behind the queued/running state. The dispatcher only cleared one window of rows at a time, so most of the column stayed stale until the cursor walked to it. Now: - Dispatcher's `pending → dispatching` transition runs a single SQL UPDATE that wipes targeted `data` output columns and `executions[gid]` across every targeted row (mode-aware: 'incomplete' skips fully-filled rows). - Per-window clear in `dispatcherStep` is gone — rows are pre-cleared, the loop only filters cancel tombstones / unmet deps and enqueues. - Optimistic patch in `useRunColumn` mirrors the bulk clear by nulling output values in the cached row, so the UI flips queued/running instantly without waiting for the SSE catch-up. * fix(table): bulk clear honors in-flight execs under mode: 'incomplete' The eager bulk clear for mode: 'incomplete' only skipped rows that were already fully filled, so two overlapping dispatches could race — dispatch B would nuke executions[gid] on a row dispatch A had just stamped 'queued', flickering the cell and potentially confusing the worker. Skip any row whose targeted group is currently queued/running/pending — an 'incomplete' run shouldn't touch what another dispatch is actively working on. The per-walk 'in-flight' eligibility skip already handles rows that flip in-flight between the clear and the cursor reaching them. * refactor(table): dispatcher uses batchTriggerAndWait + tag-based cancel Switch the per-window cell fan-out from fire-and-forget tasks.trigger to tasks.batchTriggerAndWait. The dispatcher is now a single long-lived trigger.dev task that loops dispatcherStep until the table is exhausted; trigger.dev CRIU-checkpoints the parent during each wait so we don't pay compute while cells execute. Queue depth is bounded at WINDOW_SIZE per dispatch — no more flooding trigger.dev with a million queued runs. - dispatcher.ts builds payloads via the new shared buildPendingRuns helper and calls tasks.batchTriggerAndWait directly. Pre-stamps each cell to `queued` (jobId=null) so the UI flips instantly. - table-run-dispatcher.ts is now a plain while-true loop. No RUN_BUDGET_MS, no self-re-enqueue, no cold-start tax per window. Cancel: - New cancelCellRunsByTags(tags) paginates runs.list + runs.cancel(id). - cancelWorkflowGroupRuns fires the tag-sweep alongside the per-jobId queue.cancelJob path (preserved for auto-fire cells that have real jobIds from single tasks.trigger calls). - Trigger.dev acks the cancel → batchTriggerAndWait resumes → dispatcher observes the dispatch-row cancel flag → exits. Side fixes: - getAsyncBackendType returns 'trigger-dev' whenever taskContext.isInsideTask is true, regardless of TRIGGER_DEV_ENABLED env. The preview/dev-sim worker silently routing cell jobs to DatabaseJobQueue (no poller) is fixed without any env config change. - runWorkflowColumn skips the dispatcher entirely when trigger.dev is disabled, running cells inline via DatabaseJobQueue.runInline. HTTP response returns dispatchId: null in that mode. - runColumnContract response schema updated to dispatchId.nullable(). * fix(table): show Stop button on optimistic-pending row cells isExecInFlight required a jobId for `pending` status, gating it as "real backend pending" vs "optimistic flag only." The row-gutter Stop button keyed on this — so a freshly clicked Play sat as `pending` (no jobId) and the user couldn't cancel it until the server-side `queued` stamp arrived via SSE. With the dispatcher pre-batch stamping cells as `queued` (not `pending`) and no per-cell jobIds under batchTriggerAndWait, the gap was worse. Drop the jobId requirement. `pending` now counts as in-flight everywhere. Cancel writes `cancelled` to the cell exec authoritatively whether or not a real trigger.dev run exists yet — cancelling an optimistic cell means "don't run this," which is correct. Also collapse isOptimisticInFlight into isExecInFlight since the two helpers are now identical. * refactor(table): loop-in-cell cascade + dispatcher-everywhere routing Two coupled changes: 1. Cell-task runs the row's full cascade in-process. executeWorkflowGroupCellJob acquires a Redis lock per (tableId, rowId) with heartbeat (10s/30s TTL), then loops through eligible workflow groups for the row. One cell-task = one row's full cascade, not N. Resume worker holds the same lock and continues the cascade after a HITL resume. Shared withCascadeLock helper in lib/table/cascade-lock.ts. 2. Every cell-enqueue goes through the dispatcher. The implicit scheduleRunsForRows reactor in service.ts is removed — 8 callsites (insertRow, batchInsertRows, upsertRow, updateRowsByFilter, batchUpdateRows, addWorkflowGroup, updateWorkflowGroup) now fire runWorkflowColumn with mode: 'incomplete', isManualRun: false. HTTP routes that call updateRow directly also fire runWorkflowColumn afterwards. scheduleRunsForTable / scheduleRunsForRowIds deleted; scheduleRunsForRows demoted to private (only the TRIGGER_DEV_ENABLED=false fallback uses it). skipScheduler flag dropped from UpdateRowData / BatchUpdateByIdData — no longer meaningful since there's nothing implicit to suppress. Plumbed isManualRun through the dispatch row (new is_manual_run column, default true) so auto-fire callers honor autoRun: false and don't re-run completed cells. Stamp 'pending' (not 'queued', executionId: null) before batchTriggerAndWait — cell-task writes its own 'queued' on lock acquire. Small UI polish: row gutter Play button spacing, "Delete workflow" → "Delete column" label, optimistic-pending cells now show Stop button (isExecInFlight no longer requires jobId). * fix(table): SQL cancellation guard allows worker to claim a null-execId cell The dispatcher's pre-batch `pending` stamp leaves executionId unset so any cell-task that wins the cascade lock can claim the cell. The cancellation- guard SQL clause was rejecting these claims because it tested `executions->gid IS NULL` (whole exec missing) but the pre-stamp leaves the exec present with executionId=null. Add a third carve-out: `executions->gid->>'executionId' IS NULL`. Now the guard reads "write allowed if no exec exists, OR no executionId is set yet, OR the executionId matches ours." Symptom: every cell-task's first markWorkflowGroupPickedUp call would log "SQL guard saw cancelled" and skip, leaving cells stuck at the dispatcher's pending stamp. * fix(table): dispatcher cursor starts at -1 so position 0 is included The dispatcher's row-window SELECT is `position > cursor` for exclusive lower-bound semantics. With cursor initialized to 0, position-0 rows were never picked up — every dispatch silently skipped the table's first row. Start cursor at -1 instead. First window's filter `position > -1` matches position 0; subsequent iterations advance to `lastPosition` which then correctly excludes already-processed rows. * refactor(table): align optimistic UI with new dispatcher; sticky cancel via 'new' mode Fix 0: new `DispatchMode = 'new'` for auto-fire callsites. Eligibility skips rows with any prior `executions[gid]` entry — cancelled / errored / completed cells stay sticky until a manual run. Dispatcher's windowed SELECT pushes `NOT jsonb_exists_any(...)` to SQL so CSV imports into mostly-attempted tables don't pay a per-window load+JS-filter. `batchInsertRows` drops its `rowIds` payload (keeps dispatch scope tiny on big imports). Fix A/B/D: client optimistic patches now mirror the backend's actual invariants. `useCreateTableRow.onSuccess` stamps eligible groups via `optimisticallyScheduleNewlyEligibleGroups` so newly-inserted rows show `Queued` instantly. `useCancelTableRuns.onMutate` distinguishes optimistic- only pending (`executionId == null` — strip silently) from real worker claims (stamp cancelled; SSE will reconcile). Drop `onSettled` invalidation on `useUpdateTableRow` / `useBatchUpdateTableRows` to kill the delete-cell flicker. Fix C: active-dispatches overlay. New `listActiveDispatches` helper, contract, and `GET /api/table/[tableId]/dispatches` route. `kind:'dispatch'` SSE events carry scope+cursor+mode on every transition. New `useActiveDispatches` hook + `resolveCellExec` synthesize a virtual `pending` exec for cells in an active dispatch's scope ahead of cursor — queued indicators now survive page refresh during long Run-all dispatches. `cancelWorkflowGroupRuns` emits `kind:'dispatch',status:'cancelled'` events so the overlay clears without a refetch. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(table): unify trigger.dev and inline dispatcher paths `runWorkflowColumn` now always inserts a `table_run_dispatches` row and drives the dispatcher state machine. The trigger.dev / in-process branch narrows to a single line: trigger.dev fires `tableRunDispatcherTask` (which calls the new `runDispatcherToCompletion`), the inline path calls the same helper fire-and-forget. Deletes `scheduleRunsForRows` and `stampQueuedOrCancel` — the inline-fallback no longer duplicates window walking, SSE emission, or cancel. The dispatcher's window-execute call goes through `JobQueueBackend`: - New `batchEnqueueAndWait` interface method. - Trigger.dev impl wraps `tasks.batchTriggerAndWait` behind a `taskContext.isInsideTask` guard (clear error if called from outside a task). - Database impl skips `async_jobs` entirely — `Promise.all` over `options.runner(payload, signal)` per item, with per-cell AbortControllers tracked by `cancelKey` for cancel. `cancelInlineRun` moves to the interface as `cancelByKey` so `cancelWorkflowGroupRuns` no longer reaches into the database backend. Fix `mode: 'new'` SQL filter: - `${array}::text[]` interpolated as a tuple-cast which Postgres rejected ("cannot cast type record to text[]") and every inline dispatch silently failed. Switched to `ARRAY[${sql.join(...)}]::text[]`. - Predicate was `jsonb_exists_any` ("any one targeted group present"), which excluded rows that needed at least one group re-run after a downstream output was deleted. Switched to `jsonb_exists_all` — per-group JS eligibility handles the rest. Cascade-loop workflowId bug: `runRowCascadeLoop` was not threading the new group's `workflowId` when advancing across groups. The cell-task ran the previous group's workflow against the next group's cell, terminating `completed` with empty `accumulatedData`. Fixed by tracking `currentWorkflowId` alongside `currentGroupId` / `currentExecutionId`. Client optimistic-patch tightening: - `useRunColumn.onMutate` mirrors server eligibility — skip cells with unmet deps so unmet rows don't flash Queued and get stuck (no SSE will arrive for cells the server skipped). - `resolveCellExec` overlay synthesizes a virtual `pending` only when `areGroupDepsSatisfied` is true. Rows with unmet deps render Waiting, matching the dispatcher's actual behavior. Cleanup from /simplify pass: - Use `generateShortId(20)` instead of `generateId().replace(/-/g, '').slice(0, 20)`. - Inline `batchEnqueueAndWait` no longer allocates synthetic ids (returned `string[]` is unused). - Flattened the per-cell `tracked` array — only push entries that registered controllers, drop the null placeholders. - Extracted `runDispatcherToCompletion` to share the loop between the trigger.dev wrapper and the in-process path. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(table): backend running counter, dep-aware retrigger, sidebar polish Counter (Fix 1): top-right "X running" + per-row badge are now backend-bootstrapped via a count on `user_table_rows.executions ->> 'status' = 'running'` returned alongside active dispatches. SSE `kind: 'cell'` events compute a delta from `prev → next` status to keep the cache live; cell events for rows outside the loaded page slice trigger a run-state refetch. On `pruned` we invalidate the cache. Counts only worker-claimed `running` cells — optimistic queued/pending no longer inflate the badge, and rows outside the loaded page slice are counted too. Sidebar (Fix 2 + 3a): `Run after` no longer ticks every column by default for new groups (empty list). Save is disabled with an inline error when auto-run is on with zero deps. `edit-group` mode anchors the left-of-current filter to the group's leftmost column, so a workflow can only depend on columns to its left. Reorder scrub (Fix 3b): `updateTableMetadata` walks the schema's workflow groups when `columnOrder` is in the patch and drops any dep whose new position lands at or after the group's leftmost column (uses the existing `stripGroupDeps` helper). Metadata + schema updates land atomically. Server returns ordered columns (Fix 3b cont'd): `getTableById` / `listTables` now sort `schema.columns` by `metadata.columnOrder` before returning, via a new `applyColumnOrderToSchema` helper. Every consumer (grid, sidebar, copilot, mothership) gets one ordered list — the sidebar's leftmost-group-column anchor now points at the right index. Dep-aware retrigger (Fix 4): editing a value that a downstream workflow depends on now re-runs that workflow. - `deriveExecClearsForDataPatch` returns `{ executionsPatch, inFlightDownstreamGroups }`. Walks `schema.workflowGroups[].dependencies.columns` for every column in the patch, clears terminal-state downstream entries, and reports in-flight entries. - `updateRow` calls `cancelWorkflowGroupRuns` + `runWorkflowColumn` (`mode: 'incomplete' + isManualRun: true`) for in-flight downstream groups, then always fires `runWorkflowColumn({ mode: 'new' })` for the cleared groups. Skips both when `executionsPatch` is provided by the caller — those are cell-task / cancel writes that would otherwise spawn a recursive flood of dispatches per partial-write. - `cancelWorkflowGroupRuns(tableId, rowId, { groupIds? })` accepts a per-group filter so the cancel only touches the affected groups, not every in-flight cell on the row. - `pickNextEligibleGroupForRow` now treats a dispatcher pre-stamp (`pending` + `executionId: null`) as claimable — the cascade-loop is the real owner. Without this, the dispatcher's pre-stamp of downstream groups made the cascade-loop see them as "in-flight" and skip them, stranding `pending` cells forever. - `optimisticallyScheduleNewlyEligibleGroups` extends the cache patch to flip dep-touched groups to `pending` regardless of their current status, matching the server's cancel-then-rerun behavior. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(table): paused workflow cells route through executeResumeJob; render Pending + viewable Three connected issues with workflows that pause mid-cell (e.g. wait blocks): 1. `/api/resume/poll` (the time-pause auto-resumer) called `PauseResumeManager.startResumeExecution` directly, bypassing `executeResumeJob` from `background/resume-execution.ts`. The wrapper is where the cell-context restoration + cascade-loop continuation lives — without it, the resumed workflow ran to completion but never wrote the terminal state back to the table cell. Cell stays `pending` forever even though the underlying execution finished. Fix: dynamically import `executeResumeJob` and use it for the `'starting'` branch. Same primitive the trigger.dev `resumeExecutionTask` wraps — calling it directly handles both trigger.dev-disabled local dev and trigger.dev-enabled prod identically. 2. The cell renderer mapped `status: 'pending'` to `kind: 'queued'` (gray "Queued" badge) regardless of whether the run had started. A HITL-paused run has `status: 'pending'` + `jobId` prefixed `paused-` + a real `executionId` — semantically very different from "queued, hasn't run." Now renders as `pending-upstream` (the existing Pending pill) for paused-jobId rows. 3. Right-click "View execution" was disabled for `pending` cells (gated to `completed | error | running`), so users couldn't open the trace for a paused execution. Paused runs have a viewable trace (the executionId is real and the log row exists). Both the per-row context menu and the action-bar derivation now recognize `pending` + `paused-` jobId as a started run. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(table): typewriter reveal for SSE-driven workflow cell values Workflow-output cells now reveal their text character-by-character when an SSE update lands, while page reloads and virtualization remounts still paint the value instantly. A first-render guard inside the new useTypewriter hook distinguishes hydration from live updates with no plumbing through the cell tree. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(table): address bugbot/greptile review feedback Two P1 issues + one cleanup from the bot reviewers: 1. **Double-dispatch + completed-output wipe.** Both PATCH row routes (`app/api/table/[tableId]/rows/[rowId]` and `app/api/v1/tables/[tableId]/rows/[rowId]`) were firing a second `runWorkflowColumn({ mode: 'incomplete' })` after `updateRow` returns. `updateRow` already fires `mode: 'new'` internally for user edits, so the second call created a concurrent dispatch. Worse, the `mode: 'incomplete'` path's `bulkClearWorkflowGroupCells` wipes ALL targeted output columns on any row where any one column is empty — meaning sibling-group completed outputs could be erased. Removed both route-level calls; auto-dispatch lives entirely in `updateRow`. 2. **`runWorkflowColumn` log-spamming on plain tables.** `if (targetGroups.length === 0) throw new Error(...)` fired on every row insert/update for tables without any workflow groups (the majority). Every caller wraps with `.catch(logger.error)`, so each PATCH produced an error-level log. Return `{ dispatchId: null }` silently — manual `runWorkflowColumn` callers pass `groupIds` explicitly so they can't reach this branch. 3. **`isManualRun` plumbed through dispatch SSE events.** Late-arriving `kind: 'dispatch'` events for dispatches not in the initial fetch were hardcoding `isManualRun: false`. Added the field to the event shape, emit it from `dispatcherStep` (pending → complete, dispatching transitions) and `markActiveDispatchesCancelled`, and consume it in the SSE handler with a sensible fallback for legacy emits. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(table): row executions sidecar + left-to-right dep retrigger + cancel counter refresh Split per-row workflow-group execution state out of the user_table_rows.executions JSONB column into a new table_row_executions sidecar keyed by (row_id, group_id). Dispatcher filters, "X running" counter, bulk clears, and the cancellation guard all hit indexed columns instead of walking JSONB. Wire shape unchanged — server merges sidecar rows back into row.executions on the way out. Also: - deriveExecClearsForDataPatch now walks workflowGroups left-to-right with a propagating dirtied-column set so transitive dep chains (edit col A → group 1 re-runs → group 2 depends on group 1's output → group 2 re-runs) collapse to a single forward pass. - useCancelTableRuns.onSettled invalidates the activeDispatches query so the top-right counter and row gutter Stop button refetch from the server after any Stop (per-cell, row, or table-wide). countRunningCells is the source of truth; client no longer needs duplicate state. Three migrations on this branch (0209 + 0210 + new sidecar) collapsed into one since the feature is unreleased. * fix(table): address remaining cursor/greptile review feedback - Mothership update_row no longer double-dispatches. updateRow already fires the auto-cascade internally; the second `mode: 'incomplete'` call here raced with it and could bulk-clear sibling-group outputs. - SSE dispatch events no longer dropped when the activeDispatches cache is cold. Seed an empty TableRunState if the initial fetch hasn't landed yet so the queued overlay doesn't lose the first dispatch event. - batchUpdateRows now runs cancel+rerun for per-row in-flight downstream groups, mirroring updateRow. Without this, dep edits in a batch left running workflows reading stale upstream values. * fix(table): cancel prior runs, scope batch insert dispatch, recover orphan pre-stamps Addresses cursor + greptile review feedback on table dispatcher edge cases: - Manual table-wide Run-all / Run-column now cancels prior active dispatches AND in-flight cell workers before bulk-clearing. Without this, mode:'all' deleted running sidecar rows out from under their workers (which kept writing into the wiped state) and a second Run-all could enqueue overlapping cells racing on the same rows. Row-scoped manual calls (dep-edit cascade) are excluded — those already cancel their own scope. - batchInsertRowsWithTx now scopes its auto-dispatch to the newly-inserted row ids. Without this, after the sidecar migration the NOT EXISTS filter matches every existing row (zero sidecar entries), so a CSV import would walk the entire table dispatching workflow runs on every pre-existing row. - classifyEligibility carve-out: pending + executionId=null is an orphan pre-stamp (cascade-lock contention, batchEnqueueAndWait failure, etc.), treated as claimable so future dispatchers can re-stamp instead of skipping it as 'in-flight' forever. Matches pickNextEligibleGroupForRow's logic. - On batchEnqueueAndWait failure, dispatcherStep now sweeps the orphan pre-stamps it wrote for the failed batch so the cells don't render Queued forever; the next user action picks them up cleanly. * fix(table): row-scoped Refresh cancels in-flight; counter includes queued/pending - runWorkflowColumn now cancels prior in-flight cells for row-scoped manual runs too (context-menu Refresh on a row subset, action-bar Refresh on selected rows). Previously only the table-wide path cancelled, so a row-scoped Refresh would bulk-clear running sidecar rows without aborting workers. Per-row cancel skips markActiveDispatchesCancelled so unrelated dispatches keep running. - countRunningCells now counts all in-flight statuses (queued / running / pending) instead of just running. The row gutter Run/Stop button reads this map — with the old behavior, clicking Play during the queued window would re-enqueue an already-queued cell. SSE applyCell handler updated to use isExecInFlight so client deltas track the same semantics. * fix(table): per-row Stop tombstones ahead-of-cursor rows during Run-all Per-row Stop only cancelled sidecar rows already in flight. A row the dispatcher hadn't reached yet had no exec record, so Stop was a no-op there — the dispatcher would later walk to it, classify the group eligible, and re-fire workflows the user thought they stopped. cancelWorkflowGroupRuns now, for a per-row cancel, checks active dispatches whose scope covers the row and writes `cancelled` tombstones (cancelledAt = now) for the at-risk groups that don't already have a sidecar entry. The dispatcher's existing `cancelledAt > dispatch.requestedAt` filter then skips them when the cursor arrives. onConflictDoNothing guards against clobbering a concurrently-written entry; the active-dispatch check avoids stamping spurious cancels on idle rows. * fix(table): seed dispatch overlay on Run; surface batch-enqueue failures as error - useRunColumn.onSuccess invalidates the activeDispatches query so the resolveCellExec queued overlay populates immediately for ahead-of-cursor rows (scrolled-in / refetched), instead of waiting for the first dispatch SSE. Targeted at activeDispatches only — the rows cache stays owned by useTableEventStream. - On batchEnqueueAndWait failure, dispatcherStep now flips the orphan pre-stamps to a terminal `error` state and emits a cell SSE event, rather than deleting them. The cursor still advances past the window, but the dropped cells are now visible (Error pill) instead of silently empty, stay out of the in-flight set, and re-run on the next manual run. * fix(table): seed dispatch overlay on Run; surface batch-enqueue failures as error - useRunColumn.onSuccess invalidates activeDispatches so the resolveCellExec queued overlay populates immediately for ahead-of-cursor rows instead of waiting for the first dispatch SSE. Rows cache stays owned by SSE. - On batchEnqueueAndWait failure, dispatcherStep flips orphan pre-stamps to a terminal error state (+ cell SSE) instead of deleting them, so the dropped window is visible (Error pill) rather than silently empty and re-runs on the next manual run. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- apps/sim/app/api/resume/poll/route.ts | 12 +- .../api/table/[tableId]/columns/run/route.ts | 10 +- .../api/table/[tableId]/dispatches/route.ts | 65 + .../api/table/[tableId]/rows/[rowId]/route.ts | 5 + .../sim/app/api/table/[tableId]/rows/route.ts | 51 +- .../v1/tables/[tableId]/rows/[rowId]/route.ts | 3 + .../table-grid/cells/cell-render.tsx | 56 +- .../components/table-grid/data-row.tsx | 30 +- .../table-grid/headers/column-header-menu.tsx | 4 +- .../headers/workflow-group-meta-cell.tsx | 8 +- .../components/table-grid/table-grid.tsx | 63 +- .../[tableId]/components/table-grid/utils.ts | 37 + .../workflow-sidebar/run-settings-section.tsx | 18 +- .../workflow-sidebar/workflow-sidebar.tsx | 65 +- .../[tableId]/hooks/use-table-event-stream.ts | 94 +- apps/sim/background/resume-execution.ts | 374 +- apps/sim/background/table-run-dispatcher.ts | 37 + .../background/workflow-column-execution.ts | 193 +- apps/sim/hooks/queries/tables.ts | 140 +- apps/sim/lib/api/contracts/tables.ts | 51 +- .../copilot/tools/server/table/user-table.ts | 14 +- .../lib/core/async-jobs/backends/database.ts | 60 +- .../core/async-jobs/backends/trigger-dev.ts | 52 + apps/sim/lib/core/async-jobs/config.ts | 10 +- apps/sim/lib/core/async-jobs/types.ts | 39 + apps/sim/lib/table/cascade-lock.ts | 61 + apps/sim/lib/table/cell-write.ts | 5 +- apps/sim/lib/table/deps.ts | 41 +- apps/sim/lib/table/dispatcher.ts | 543 + apps/sim/lib/table/events.ts | 66 +- apps/sim/lib/table/service.ts | 761 +- apps/sim/lib/table/types.ts | 27 +- apps/sim/lib/table/workflow-columns.ts | 714 +- packages/db/migrations/0209_smiling_fixer.sql | 41 + .../db/migrations/meta/0209_snapshot.json | 16431 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 93 +- packages/testing/src/mocks/database.mock.ts | 13 +- packages/testing/src/mocks/schema.mock.ts | 28 + scripts/check-api-validation-contracts.ts | 4 +- 40 files changed, 19416 insertions(+), 910 deletions(-) create mode 100644 apps/sim/app/api/table/[tableId]/dispatches/route.ts create mode 100644 apps/sim/background/table-run-dispatcher.ts create mode 100644 apps/sim/lib/table/cascade-lock.ts create mode 100644 apps/sim/lib/table/dispatcher.ts create mode 100644 packages/db/migrations/0209_smiling_fixer.sql create mode 100644 packages/db/migrations/meta/0209_snapshot.json diff --git a/apps/sim/app/api/resume/poll/route.ts b/apps/sim/app/api/resume/poll/route.ts index ad07ea009b5..86edf2f218c 100644 --- a/apps/sim/app/api/resume/poll/route.ts +++ b/apps/sim/app/api/resume/poll/route.ts @@ -139,13 +139,21 @@ async function dispatchRow(row: DueRow, now: Date): Promise { }) if (enqueueResult.status === 'starting') { - PauseResumeManager.startResumeExecution({ + // Route through `executeResumeJob` (not `PauseResumeManager.startResumeExecution` + // directly) so cell-context restoration + cascade-loop continuation + // fires. This is the same primitive the trigger.dev `resumeExecutionTask` + // wraps — calling it directly handles both trigger.dev-disabled local + // dev and trigger.dev-enabled prod identically. + const { executeResumeJob } = await import('@/background/resume-execution') + void executeResumeJob({ resumeEntryId: enqueueResult.resumeEntryId, resumeExecutionId: enqueueResult.resumeExecutionId, - pausedExecution: enqueueResult.pausedExecution, + pausedExecutionId: enqueueResult.pausedExecution.id, contextId: enqueueResult.contextId, resumeInput: enqueueResult.resumeInput, userId: enqueueResult.userId, + workflowId: row.workflowId, + parentExecutionId: row.executionId, }).catch((error) => { logger.error('Background time-pause resume failed', { executionId: row.executionId, diff --git a/apps/sim/app/api/table/[tableId]/columns/run/route.ts b/apps/sim/app/api/table/[tableId]/columns/run/route.ts index eddfc416e0a..2b96981d115 100644 --- a/apps/sim/app/api/table/[tableId]/columns/run/route.ts +++ b/apps/sim/app/api/table/[tableId]/columns/run/route.ts @@ -1,5 +1,4 @@ import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { runColumnContract } from '@/lib/api/contracts/tables' import { parseRequest } from '@/lib/api/server' @@ -30,21 +29,16 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro const access = await checkAccess(tableId, auth.userId, 'write') if (!access.ok) return accessError(access, requestId, tableId) - // Dispatch in the background — large fan-outs (thousands of rows) issue - // sequential trigger.dev calls and would otherwise hold the HTTP response - // open for minutes, blocking the AI/copilot tool span and the UI mutation. - void runWorkflowColumn({ + const { dispatchId } = await runWorkflowColumn({ tableId, workspaceId, groupIds, mode: runMode, rowIds, requestId, - }).catch((err) => { - logger.error(`[${requestId}] run-column dispatch failed:`, toError(err).message) }) - return NextResponse.json({ success: true, data: { triggered: null } }) + return NextResponse.json({ success: true, data: { dispatchId } }) } catch (error) { if (error instanceof Error && error.message === 'Invalid workspace ID') { return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) diff --git a/apps/sim/app/api/table/[tableId]/dispatches/route.ts b/apps/sim/app/api/table/[tableId]/dispatches/route.ts new file mode 100644 index 00000000000..a5e442f05dd --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/dispatches/route.ts @@ -0,0 +1,65 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { type ActiveDispatch, listActiveDispatchesContract } from '@/lib/api/contracts/tables' +import { parseRequest } from '@/lib/api/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { countRunningCells, listActiveDispatches } from '@/lib/table/dispatcher' +import { accessError, checkAccess } from '@/app/api/table/utils' + +const logger = createLogger('TableDispatchesAPI') + +interface RouteParams { + params: Promise<{ tableId: string }> +} + +/** + * GET /api/table/[tableId]/dispatches + * + * Returns active (`pending` / `dispatching`) dispatches for the table. Drives + * the client's "about to run" overlay so refresh during a long Run-all keeps + * the queued indicators on rows the dispatcher hasn't reached yet. + */ +export const GET = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => { + const requestId = generateRequestId() + + try { + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + const parsed = await parseRequest(listActiveDispatchesContract, request, { params }) + if (!parsed.success) return parsed.response + const { tableId } = parsed.data.params + + const result = await checkAccess(tableId, authResult.userId, 'read') + if (!result.ok) return accessError(result, requestId, tableId) + + const [rows, running] = await Promise.all([ + listActiveDispatches(tableId), + countRunningCells(tableId), + ]) + const dispatches: ActiveDispatch[] = rows.map((r) => ({ + id: r.id, + status: r.status as 'pending' | 'dispatching', + mode: r.mode, + isManualRun: r.isManualRun, + cursor: r.cursor, + scope: r.scope, + })) + + return NextResponse.json({ + success: true, + data: { + dispatches, + runningCellCount: running.total, + runningByRowId: running.byRowId, + }, + }) + } catch (error) { + logger.error(`[${requestId}] list-dispatches failed:`, error) + return NextResponse.json({ error: 'Failed to list active dispatches' }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts b/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts index 9a4a988bc25..fe7452230ee 100644 --- a/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts @@ -136,6 +136,11 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: RowR // Only `null` when a `cancellationGuard` is supplied and the SQL guard // rejects the write — this route doesn't pass one, so reaching null is a bug. if (!updatedRow) throw new Error('updateRow returned null without a cancellationGuard') + // Auto-dispatch for user edits is handled inside `updateRow` (mode: 'new'). + // Firing a second mode: 'incomplete' dispatch here would race with the + // `mode: 'new'` one AND bulk-clear sibling-group outputs (the incomplete + // bulk-clear wipes ALL targeted columns when any one column on the row + // is empty). return NextResponse.json({ success: true, diff --git a/apps/sim/app/api/table/[tableId]/rows/route.ts b/apps/sim/app/api/table/[tableId]/rows/route.ts index 414ac57d41a..8e29e12005c 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' -import { userTableRows } from '@sim/db/schema' +import { tableRowExecutions, userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { and, eq, sql } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { type BatchInsertTableRowsBodyInput, @@ -17,7 +17,14 @@ import { isZodError, validationErrorResponse } from '@/lib/api/server/validation import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import type { Filter, RowData, Sort, TableSchema } from '@/lib/table' +import type { + Filter, + RowData, + RowExecutionMetadata, + RowExecutions, + Sort, + TableSchema, +} from '@/lib/table' import { batchInsertRows, batchUpdateRows, @@ -283,7 +290,6 @@ export const GET = withRouteHandler( .select({ id: userTableRows.id, data: userTableRows.data, - executions: userTableRows.executions, position: userTableRows.position, createdAt: userTableRows.createdAt, updatedAt: userTableRows.updatedAt, @@ -313,6 +319,41 @@ export const GET = withRouteHandler( const rows = await query.limit(validated.limit).offset(validated.offset) + // Sidecar: fetch per-(row, group) execution state and group into a map + // so the response preserves the legacy `row.executions[groupId]` wire + // shape. One indexed-IN scan against table_row_executions. + const executionsByRow = new Map() + if (rows.length > 0) { + const execRows = await db + .select() + .from(tableRowExecutions) + .where( + inArray( + tableRowExecutions.rowId, + rows.map((r) => r.id) + ) + ) + for (const e of execRows) { + const existing = executionsByRow.get(e.rowId) ?? {} + const meta: RowExecutionMetadata = { + status: e.status as RowExecutionMetadata['status'], + executionId: e.executionId ?? null, + jobId: e.jobId ?? null, + workflowId: e.workflowId, + error: e.error ?? null, + ...(e.runningBlockIds && e.runningBlockIds.length > 0 + ? { runningBlockIds: e.runningBlockIds } + : {}), + ...(e.blockErrors && Object.keys(e.blockErrors as Record).length > 0 + ? { blockErrors: e.blockErrors as Record } + : {}), + ...(e.cancelledAt ? { cancelledAt: e.cancelledAt.toISOString() } : {}), + } + existing[e.groupId] = meta + executionsByRow.set(e.rowId, existing) + } + } + logger.info( `[${requestId}] Queried ${rows.length} rows from table ${tableId} (total: ${totalCount ?? 'n/a'})` ) @@ -323,7 +364,7 @@ export const GET = withRouteHandler( rows: rows.map((r) => ({ id: r.id, data: r.data, - executions: r.executions ?? {}, + executions: executionsByRow.get(r.id) ?? {}, position: r.position, createdAt: r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt), diff --git a/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts b/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts index 810bb0dfc65..96e2cf323c0 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts @@ -144,6 +144,9 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: RowR if (!updatedRow) { return NextResponse.json({ error: 'Row not found' }, { status: 404 }) } + // Auto-dispatch for user edits is handled inside `updateRow` (mode: 'new'). + // Firing a second mode: 'incomplete' dispatch here would race with it AND + // bulk-clear sibling-group outputs. return NextResponse.json({ success: true, diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index c2c78971d90..6d63bca978b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -1,6 +1,7 @@ 'use client' import type React from 'react' +import { useEffect, useRef, useState } from 'react' import { parse } from 'tldts' import { Badge, Checkbox, Tooltip } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' @@ -60,6 +61,14 @@ export function resolveCellRender({ if (!isNull) return { kind: 'value', text: stringifyValue(value) } if (inFlight && !(groupHasBlockErrors && !blockRunning)) { + // A `pending` cell whose jobId starts with `paused-` is mid-pause + // (workflow yielded for human-in-the-loop). Render as Pending rather + // than Queued so the user can tell it's not just waiting to start. + const isPaused = + exec?.status === 'pending' && + typeof exec.jobId === 'string' && + exec.jobId.startsWith('paused-') + if (isPaused) return { kind: 'pending-upstream' } if (exec?.status === 'queued' || exec?.status === 'pending') return { kind: 'queued' } return { kind: 'pending-upstream' } } @@ -119,6 +128,9 @@ interface CellRenderProps { } export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactElement | null { + const valueText = kind.kind === 'value' ? kind.text : null + const revealedValueText = useTypewriter(valueText) + switch (kind.kind) { case 'value': return ( @@ -128,7 +140,7 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle isEditing && 'invisible' )} > - {kind.text} + {revealedValueText ?? kind.text} ) @@ -275,3 +287,45 @@ function Wrap({ isEditing, children }: { isEditing: boolean; children: React.Rea if (!isEditing) return <>{children} return
{children}
} + +const TYPEWRITER_MS_PER_CHAR = 15 + +/** + * Reveals `text` character-by-character whenever it changes after the first + * render. Initial render (page hydration or virtualization remount) shows the + * value statically — animation fires only for subsequent updates, which in + * practice means SSE-driven workflow completions arriving via + * `useTableEventStream → applyCell()`. + */ +function useTypewriter(text: string | null): string | null { + const [revealed, setRevealed] = useState(text) + const isFirstRunRef = useRef(true) + const prevTextRef = useRef(text) + + useEffect(() => { + if (isFirstRunRef.current) { + isFirstRunRef.current = false + prevTextRef.current = text + setRevealed(text) + return + } + if (prevTextRef.current === text) return + prevTextRef.current = text + + if (text === null || text.length === 0) { + setRevealed(text) + return + } + + setRevealed('') + let i = 0 + const id = window.setInterval(() => { + i++ + setRevealed(text.slice(0, i)) + if (i >= text.length) window.clearInterval(id) + }, TYPEWRITER_MS_PER_CHAR) + return () => window.clearInterval(id) + }, [text]) + + return revealed +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index b340fb95d2a..aed56ac8de4 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -3,6 +3,7 @@ import React from 'react' import { Button, Checkbox } from '@/components/emcn' import { PlayOutline, Square } from '@/components/emcn/icons' +import type { ActiveDispatch } from '@/lib/api/contracts/tables' import { cn } from '@/lib/core/utils/cn' import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' import type { TableRow as TableRowType, WorkflowGroup } from '@/lib/table' @@ -17,7 +18,7 @@ import { SELECTION_TINT_BG, } from './constants' import type { DisplayColumn } from './types' -import { type NormalizedSelection, readExecution } from './utils' +import { type NormalizedSelection, resolveCellExec } from './utils' export interface DataRowProps { row: TableRowType @@ -50,6 +51,12 @@ export interface DataRowProps { * for empty workflow-output cells whose group has unmet dependencies. */ workflowGroups: WorkflowGroup[] + /** + * Active dispatches on the table — rows in scope ahead of the dispatcher's + * cursor render as `Queued` until the dispatcher pre-stamps them. Preserves + * queued indicators across page refresh during long Run-all dispatches. + */ + activeDispatches: ActiveDispatch[] | undefined } function cellRangeRowChanged( @@ -105,7 +112,8 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { prev.numDivWidth !== next.numDivWidth || prev.onStopRow !== next.onStopRow || prev.onRunRow !== next.onRunRow || - prev.workflowGroups !== next.workflowGroups + prev.workflowGroups !== next.workflowGroups || + prev.activeDispatches !== next.activeDispatches ) { return false } @@ -148,6 +156,7 @@ export const DataRow = React.memo(function DataRow({ onStopRow, onRunRow, workflowGroups, + activeDispatches, }: DataRowProps) { const sel = normalizedSelection /** @@ -178,8 +187,8 @@ export const DataRow = React.memo(function DataRow({
0 ? `Stop ${runningCount} running` : 'Run row'} title={runningCount > 0 ? `Stop ${runningCount} running` : 'Run row'} - // mr-px keeps the hover bg off the cell's right border — without - // it the rounded-rect background paints over the divider line - // while the button is hovered. - className='mr-px size-[20px] shrink-0 p-0 text-[var(--text-primary)] hover-hover:bg-[var(--surface-2)]' + className='size-[20px] shrink-0 p-0 text-[var(--text-primary)] hover-hover:bg-[var(--surface-2)]' onClick={() => { if (runningCount > 0) { onStopRow(row.id) @@ -309,7 +315,13 @@ export const DataRow = React.memo(function DataRow({ ? pendingCellValue[column.name] : row.data[column.name] } - exec={readExecution(row, column.workflowGroupId)} + exec={resolveCellExec( + row, + column.workflowGroupId + ? workflowGroups.find((g) => g.id === column.workflowGroupId) + : undefined, + activeDispatches + )} column={column} isEditing={isEditing} initialCharacter={isEditing ? initialCharacter : undefined} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx index e3c437f2588..06a004843e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx @@ -89,12 +89,12 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ ? workflows?.find((w) => w.id === ownGroup.workflowId) : undefined // Workflow-output column with siblings → "Hide column" (non-destructive, - // re-addable from sidebar). Last output of a group → "Delete workflow" + // re-addable from sidebar). Last output of a group → "Delete column" // (removes the entire group). Plain column → undefined (default "Delete column"). const deleteLabel = ownGroup ? ownGroup.outputs.length > 1 ? 'Hide column' - : 'Delete workflow' + : 'Delete column' : undefined useEffect(() => { if (isRenaming && renameInputRef.current) { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 70f2cc9ac6a..23fa84f2227 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -35,9 +35,9 @@ interface ColumnOptionsMenuProps { position: { x: number; y: number } column: DisplayColumn /** Override for the destructive item's label. Defaults to "Delete column" - * (or "Delete workflow" when `onDeleteGroup` is set). Use "Hide column" - * when the destructive action is non-lossy (workflow-output column where - * removing it leaves the group with siblings). */ + * for both plain columns and workflow groups. Use "Hide column" when the + * destructive action is non-lossy (workflow-output column where removing + * it leaves the group with siblings). */ deleteLabel?: string onOpenConfig: (columnName: string) => void onInsertLeft: (columnName: string) => void @@ -156,7 +156,7 @@ export function ColumnOptionsMenu({ onSelect={() => (onDeleteGroup ? onDeleteGroup() : onDeleteColumn(column.name))} > {deleteLabel === 'Hide column' ? : } - {deleteLabel ?? (onDeleteGroup ? 'Delete workflow' : 'Delete column')} + {deleteLabel ?? 'Delete column'} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 2640e0a1453..698c6b31e4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -12,7 +12,6 @@ import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' import type { ColumnDefinition, TableRow as TableRowType } from '@/lib/table' import { TABLE_LIMITS } from '@/lib/table/constants' -import { isExecInFlight } from '@/lib/table/deps' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useAddTableColumn, @@ -21,6 +20,7 @@ import { useCreateTableRow, useDeleteColumn, useDeleteWorkflowGroup, + useTableRunState, useUpdateColumn, useUpdateTableMetadata, useUpdateTableRow, @@ -73,6 +73,8 @@ import { const logger = createLogger('TableView') +const EMPTY_RUNNING_BY_ROW: Readonly> = Object.freeze({}) + const COL_WIDTH_MIN = 80 const COL_WIDTH_AUTO_FIT_MAX = 1000 const SKELETON_COL_COUNT = 4 @@ -300,6 +302,11 @@ export function TableGrid({ ensureAllRowsLoaded, } = useTable({ workspaceId, tableId, queryOptions }) + const { data: tableRunState } = useTableRunState(tableId) + const activeDispatches = tableRunState?.dispatches + const totalRunning = tableRunState?.runningCellCount ?? 0 + const runningByRowId = tableRunState?.runningByRowId ?? EMPTY_RUNNING_BY_ROW + const fetchNextPageRef = useRef(fetchNextPage) fetchNextPageRef.current = fetchNextPage const hasNextPageRef = useRef(hasNextPage) @@ -652,12 +659,20 @@ export function TableGrid({ if (_col && _gid) { const _exec = contextMenu.row.executions?.[_gid] contextMenuIsWorkflowColumn = true - // Only `completed` / `error` / `running` cells are guaranteed to have a - // server-side execution log. `queued` / `pending` haven't started yet; - // `cancelled` may have been cancelled before the worker ever picked the - // job up, so its executionId can't be relied on either. + // Cells with a server-side execution log: `completed` / `error` / + // `running`, plus HITL-paused runs (status `pending` with a `paused-` + // jobId — has a real executionId + viewable trace). `queued` / plain + // `pending` haven't started yet; `cancelled` may have been cancelled + // before the worker ever picked the job up. + const _isPaused = + _exec?.status === 'pending' && + typeof _exec?.jobId === 'string' && + _exec.jobId.startsWith('paused-') contextMenuHasStartedRun = - _exec?.status === 'completed' || _exec?.status === 'error' || _exec?.status === 'running' + _exec?.status === 'completed' || + _exec?.status === 'error' || + _exec?.status === 'running' || + _isPaused contextMenuExecutionId = _exec?.executionId ?? null } } @@ -2690,22 +2705,11 @@ export function TableGrid({ return ids.length > 0 ? ids : null }, [rowSelection, rows]) - const { runningByRowId, totalRunning } = useMemo(() => { - const byRow = new Map() - let total = 0 - for (const row of rows) { - let count = 0 - const executions = row.executions ?? {} - for (const gid in executions) { - if (isExecInFlight(executions[gid])) count++ - } - if (count > 0) { - byRow.set(row.id, count) - total += count - } - } - return { runningByRowId: byRow, totalRunning: total } - }, [rows]) + // `runningByRowId` + `totalRunning` come from `useTableRunState` above — + // backend-bootstrapped via `countRunningCells` and kept live by + // `applyCell`'s SSE-driven delta. Counts only cells whose worker has + // actually claimed the cell (`status === 'running'`), ignoring optimistic + // queued/pending stamps. // Context-menu wrappers: act on `contextMenuRowIds`, then close the menu. // Mirror the action bar's Play / Refresh split: Play fills empty/failed, @@ -2726,7 +2730,7 @@ export function TableGrid({ // Total running/queued cells across the rows the context menu is acting on; // drives the "Stop N running workflows" item, shown only when > 0. const runningInContextSelection = contextMenuRowIds.reduce( - (total, rowId) => total + (runningByRowId.get(rowId) ?? 0), + (total, rowId) => total + (runningByRowId[rowId] ?? 0), 0 ) @@ -2757,7 +2761,7 @@ export function TableGrid({ return [] }, [rowSelection, normalizedSelection, rows]) const runningInActionBarSelection = actionBarRowIds.reduce( - (total, rowId) => total + (runningByRowId.get(rowId) ?? 0), + (total, rowId) => total + (runningByRowId[rowId] ?? 0), 0 ) @@ -2785,11 +2789,17 @@ export function TableGrid({ } const exec = row.executions?.[groupId] const status = exec?.status + // A `pending` execution with a `paused-` jobId is a HITL-paused run — + // it has a real executionId and a viewable trace, same as + // running/completed/error. + const isPaused = + status === 'pending' && typeof exec?.jobId === 'string' && exec.jobId.startsWith('paused-') return { rowId: row.id, groupId, executionId: exec?.executionId ?? null, - canViewExecution: status === 'completed' || status === 'error' || status === 'running', + canViewExecution: + status === 'completed' || status === 'error' || status === 'running' || isPaused, } }, [normalizedSelection, rows, displayColumns]) @@ -3149,12 +3159,13 @@ export function TableGrid({ onCellMouseEnter={handleCellMouseEnter} isRowChecked={rowSelectionIncludes(rowSelection, row.id)} onRowToggle={handleRowToggle} - runningCount={runningByRowId.get(row.id) ?? 0} + runningCount={runningByRowId[row.id] ?? 0} hasWorkflowColumns={hasWorkflowColumns} numDivWidth={numDivWidth} onStopRow={onStopRow} onRunRow={onRunRow} workflowGroups={tableWorkflowGroups} + activeDispatches={activeDispatches} /> ))} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts index a0efce3b877..50f7aae85af 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts @@ -1,3 +1,4 @@ +import type { ActiveDispatch } from '@/lib/api/contracts/tables' import type { ColumnDefinition, RowExecutionMetadata, @@ -5,6 +6,7 @@ import type { TableRow as TableRowType, WorkflowGroup, } from '@/lib/table' +import { areGroupDepsSatisfied, areOutputsFilled } from '@/lib/table/deps' import type { DeletedRowSnapshot } from '@/stores/table/types' import type { DisplayColumn } from './types' @@ -166,6 +168,41 @@ export function readExecution( return row?.executions?.[groupId] } +/** + * Resolves a cell's execution state with the "about to run" overlay applied: + * for cells in an active dispatch's scope ahead of its cursor whose deps are + * already satisfied, returns a synthetic `pending` exec so the renderer + * shows `Queued`. Cells with a real DB exec always win — the overlay only + * fills the gap between dispatch start and the dispatcher's per-row pending + * stamp. Cells with unmet deps still render as `Waiting` (the renderer + * computes that from `waitingOnLabels`). + */ +export function resolveCellExec( + row: TableRowType, + group: WorkflowGroup | undefined, + activeDispatches: ActiveDispatch[] | undefined +): RowExecutionMetadata | undefined { + if (!group) return undefined + const real = row.executions?.[group.id] + if (real) return real + if (!activeDispatches || activeDispatches.length === 0) return undefined + if (areOutputsFilled(group, row)) return undefined + if (!areGroupDepsSatisfied(group, row)) return undefined + for (const d of activeDispatches) { + if (!d.scope.groupIds.includes(group.id)) continue + if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue + if (row.position <= d.cursor) continue + return { + status: 'pending', + executionId: null, + jobId: null, + workflowId: group.workflowId, + error: null, + } + } + return undefined +} + export interface ExecStatusMix { hasIncompleteOrFailed: boolean hasCompleted: boolean diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/run-settings-section.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/run-settings-section.tsx index 320eda4bd1f..ad8e460585a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/run-settings-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/run-settings-section.tsx @@ -10,15 +10,22 @@ interface RunSettingsSectionProps { /** Column names this group waits on. */ deps: string[] onChangeDeps: (next: string[]) => void + /** Inline validation error rendered under the picker. */ + error?: string | null } /** * "Run after" picker: which upstream columns must be filled before this group * fires. Workflow output columns count the same as plain columns — once a - * column is non-empty, the dep is satisfied. Empty selection = the group fires - * on any row change. + * column is non-empty, the dep is satisfied. At least one dep is required + * when auto-run is on. */ -export function RunSettingsSection({ depOptions, deps, onChangeDeps }: RunSettingsSectionProps) { +export function RunSettingsSection({ + depOptions, + deps, + onChangeDeps, + error, +}: RunSettingsSectionProps) { const options = depOptions.map((c) => ({ label: c.name, value: c.name })) return ( @@ -38,11 +45,12 @@ export function RunSettingsSection({ depOptions, deps, onChangeDeps }: RunSettin multiSelectValues={deps} onMultiSelectChange={onChangeDeps} overlayContent={ - - {deps.length === 0 ? 'Any row change' : `${deps.length} selected`} + + {deps.length === 0 ? 'Select at least one column' : `${deps.length} selected`} } /> + {error &&

{error}

}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index fda9f87a13f..b339d3397ef 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -241,31 +241,42 @@ function WorkflowSidebarBody({ ? (allColumns.find((c) => c.name === config.columnName) ?? null) : null - // Anchor column for "left of current" filtering. For create + edit-group we - // treat the anchor as missing (group config sits at the right edge of the - // group); for edit-output the anchor is the column being edited. - const anchorColumnName = config.mode === 'edit-output' ? config.columnName : null + // Anchor index for "left of current" filtering. + // - edit-output: the column being edited. + // - edit-group: the leftmost column belonging to this group (deps must be + // reachable from the group's first output column). + // - create: no anchor; new column sits at the right edge, so every + // existing column qualifies. + const anchorIdx = (() => { + if (config.mode === 'edit-output') { + const idx = allColumns.findIndex((c) => c.name === config.columnName) + return idx === -1 ? allColumns.length : idx + } + if (config.mode === 'edit-group' && existingGroup) { + let leftmost = Number.POSITIVE_INFINITY + for (let i = 0; i < allColumns.length; i++) { + if (allColumns[i].workflowGroupId === existingGroup.id && i < leftmost) leftmost = i + } + return Number.isFinite(leftmost) ? leftmost : allColumns.length + } + return allColumns.length + })() /** * Columns "left of current" — these are the only valid trigger dependencies. - * For create + edit-group, every existing column qualifies. For edit-output, - * only columns physically before the anchor. */ - const otherColumns = (() => { - if (anchorColumnName === null) return allColumns - const idx = allColumns.findIndex((c) => c.name === anchorColumnName) - if (idx === -1) return allColumns.filter((c) => c.name !== anchorColumnName) - return allColumns.slice(0, idx) - })() + const otherColumns = anchorIdx >= allColumns.length ? allColumns : allColumns.slice(0, anchorIdx) + + // Used by the "missing workflow input" suggestion below — for edit-output + // we exclude the column being edited (you can't suggest it as its own + // input). + const anchorColumnName = config.mode === 'edit-output' ? config.columnName : null // Every left-of-current column is a valid dep — workflow output columns // included. Exclude this group's own outputs (you can't depend on yourself). const ownOutputNames = new Set(existingGroup?.outputs.map((o) => o.columnName) ?? []) const depOptions = otherColumns.filter((c) => !ownOutputNames.has(c.name)) - // Default deps for a brand-new group: tick every left-of-current column. - const defaultDeps = depOptions.map((c) => c.name) - const [selectedWorkflowId, setSelectedWorkflowId] = useState( () => existingGroup?.workflowId ?? '' ) @@ -276,9 +287,10 @@ function WorkflowSidebarBody({ const [autoRun, setAutoRun] = useState(() => existingGroup ? existingGroup.autoRun !== false : false ) - const [deps, setDeps] = useState( - () => existingGroup?.dependencies?.columns ?? defaultDeps - ) + // Deps default to none selected. With auto-run on, at least one is required + // (enforced via `depsValid` below); a legacy group with empty deps will + // surface the error on first open until the user picks at least one column. + const [deps, setDeps] = useState(() => existingGroup?.dependencies?.columns ?? []) // `selectedOutputs` is encoded `${blockId}::${path}`. Seeded once `blockOutputGroups` // resolves (we may not have the workflow blocks loaded at first render); see the // post-load reconciliation below. @@ -542,6 +554,7 @@ function WorkflowSidebarBody({ if (!selectedWorkflowId) missing.push('a workflow') if (selectedWorkflowId && selectedOutputs.length === 0) missing.push('at least one output') if (isEditOutputMode && !trimmedName) missing.push('a column name') + if (autoRun && deps.length === 0) missing.push('at least one Run after column') if (missing.length > 0) { setShowValidation(true) return @@ -664,8 +677,15 @@ function WorkflowSidebarBody({ } } + // Auto-run requires ≥1 dependency column — without one, the dispatcher's + // eligibility predicate would never fire the workflow. Block Save and + // surface an inline error so the user picks a column. + const depsValid = !autoRun || deps.length > 0 const saveDisabled = - addWorkflowGroup.isPending || updateWorkflowGroup.isPending || updateColumn.isPending + addWorkflowGroup.isPending || + updateWorkflowGroup.isPending || + updateColumn.isPending || + !depsValid const titleByMode = { create: 'Add workflow', 'edit-group': 'Configure workflow', @@ -875,7 +895,12 @@ function WorkflowSidebarBody({ {autoRun && ( <> - + )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts index e5a0e5e807f..7c00d06e338 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts @@ -3,9 +3,11 @@ import { useEffect } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' +import type { ActiveDispatch } from '@/lib/api/contracts/tables' import type { RowData, RowExecutionMetadata, RowExecutions } from '@/lib/table' +import { isExecInFlight } from '@/lib/table/deps' import type { TableEvent, TableEventEntry } from '@/lib/table/events' -import { snapshotAndMutateRows, tableKeys } from '@/hooks/queries/tables' +import { snapshotAndMutateRows, type TableRunState, tableKeys } from '@/hooks/queries/tables' const logger = createLogger('useTableEventStream') @@ -71,6 +73,28 @@ export function useTableEventStream({ let lastEventId = loadPointer(tableId) let reconnectAttempt = 0 + const updateRunStateCounters = ( + rowId: string, + wasInFlight: boolean, + isInFlight: boolean + ): void => { + if (wasInFlight === isInFlight) return + const delta = isInFlight ? 1 : -1 + queryClient.setQueryData(tableKeys.activeDispatches(tableId), (prev) => { + if (!prev) return prev + const prevForRow = prev.runningByRowId[rowId] ?? 0 + const nextForRow = Math.max(0, prevForRow + delta) + const nextByRow = { ...prev.runningByRowId } + if (nextForRow === 0) delete nextByRow[rowId] + else nextByRow[rowId] = nextForRow + return { + ...prev, + runningCellCount: Math.max(0, prev.runningCellCount + delta), + runningByRowId: nextByRow, + } + }) + } + const applyCell = (event: Extract): void => { const { rowId, @@ -83,12 +107,18 @@ export function useTableEventStream({ runningBlockIds, blockErrors, } = event + let wasInFlight: boolean | null = null void snapshotAndMutateRows( queryClient, tableId, (row) => { if (row.id !== rowId) return null const prevExec = row.executions?.[groupId] + // In-flight = queued | running | pending. Server's countRunningCells + // counts all three (the gutter Run/Stop button reads this map and + // needs Stop visible during queued too, else clicking Play would + // re-enqueue a cell that's already queued). + if (wasInFlight === null) wasInFlight = isExecInFlight(prevExec) const nextExec: RowExecutionMetadata = { status, executionId: executionId ?? null, @@ -108,11 +138,69 @@ export function useTableEventStream({ }, { cancelInFlight: false } ) + if (wasInFlight === null) { + // Row outside the loaded page slice — can't compute the delta locally. + // Refetch the run-state snapshot from the server. Cheap and rare. + void queryClient.invalidateQueries({ + queryKey: tableKeys.activeDispatches(tableId), + }) + } else { + updateRunStateCounters( + rowId, + wasInFlight, + isExecInFlight({ status } as RowExecutionMetadata) + ) + } + } + + const applyDispatch = (event: Extract): void => { + const { dispatchId, status, scope, cursor, mode, isManualRun } = event + queryClient.setQueryData(tableKeys.activeDispatches(tableId), (prev) => { + // SSE may arrive before the initial fetch lands. Seed an empty + // run-state so the dispatch isn't dropped; counters are reconciled + // by the subsequent fetch / per-cell SSE events. + const base: TableRunState = prev ?? { + dispatches: [], + runningCellCount: 0, + runningByRowId: {}, + } + const list = base.dispatches + // Terminal states drop the dispatch from the overlay; client renders + // the row's authoritative DB exec state from here. + if (status === 'complete' || status === 'cancelled') { + const filtered = list.filter((d) => d.id !== dispatchId) + return filtered.length === list.length ? base : { ...base, dispatches: filtered } + } + if (scope === undefined || cursor === undefined || mode === undefined) { + // Defensive: a legacy emit without the new fields can't drive the + // overlay. Leave existing cache alone. + return base + } + const idx = list.findIndex((d) => d.id === dispatchId) + const existing = idx === -1 ? undefined : list[idx] + // Prefer the event payload (current truth from server); fall back to + // the cached entry's value if this is a legacy emit without the + // field, and finally to `false` if we have nothing. + const resolvedManualRun = isManualRun ?? existing?.isManualRun ?? false + const next: ActiveDispatch = { + id: dispatchId, + status, + mode, + isManualRun: resolvedManualRun, + cursor, + scope, + } + if (idx === -1) return { ...base, dispatches: [...list, next] } + const merged = list.slice() + merged[idx] = next + return { ...base, dispatches: merged } + }) } const handlePrune = (payload: PrunedEvent): void => { logger.info('Table event buffer pruned — full refetch', { tableId, ...payload }) void queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) }) + void queryClient.invalidateQueries({ queryKey: tableKeys.activeDispatches(tableId) }) lastEventId = typeof payload.earliestEventId === 'number' ? payload.earliestEventId : 0 savePointer(tableId, lastEventId) // Close proactively so the server's close doesn't fire onerror and route @@ -152,11 +240,11 @@ export function useTableEventStream({ eventSource.onmessage = (msg: MessageEvent) => { try { const entry = JSON.parse(msg.data) as TableEventEntry - if (entry.event?.kind !== 'cell') return if (entry.eventId <= lastEventId) return lastEventId = entry.eventId savePointer(tableId, lastEventId) - applyCell(entry.event) + if (entry.event?.kind === 'cell') applyCell(entry.event) + else if (entry.event?.kind === 'dispatch') applyDispatch(entry.event) } catch (err) { logger.warn('Failed to parse table event', { tableId, err }) } diff --git a/apps/sim/background/resume-execution.ts b/apps/sim/background/resume-execution.ts index f7bd79d2a37..fa1ed60f595 100644 --- a/apps/sim/background/resume-execution.ts +++ b/apps/sim/background/resume-execution.ts @@ -1,6 +1,8 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' import { task } from '@trigger.dev/sdk' +import { withCascadeLock } from '@/lib/table/cascade-lock' import type { RowData, RowExecutionMetadata } from '@/lib/table/types' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' @@ -38,149 +40,65 @@ export async function executeResumeJob(payload: ResumeExecutionPayload) { // context so post-resume block outputs land on the same row + group as // the original cell task. Without this, blocks that run after the human // approves write nothing back to the table — the row silently truncates - // at the pause boundary. The original `parentExecutionId` is preserved - // on the cell's `executions[gid]` so it stays one logical execution - // across the pause/resume boundary. + // at the pause boundary. const { findCellContextByExecutionId } = await import('@/lib/table/workflow-columns') const cellContext = await findCellContextByExecutionId(parentExecutionId) - let cellOnBlockComplete: ((blockId: string, output: unknown) => Promise) | undefined - let writeCellTerminal: - | ((status: 'completed' | 'error' | 'paused', error: string | null) => Promise) - | undefined + const writers = cellContext + ? await buildResumeCellWriters(cellContext, parentExecutionId) + : null - if (cellContext) { - const { getTableById } = await import('@/lib/table/service') - const { writeWorkflowGroupState, buildOutputsByBlockId } = await import( - '@/lib/table/cell-write' - ) - const { pluckByPath } = await import('@/lib/table/pluck') - - const table = await getTableById(cellContext.tableId) - const group = table?.schema.workflowGroups?.find((g) => g.id === cellContext.groupId) - if (group) { - const outputsByBlockId = buildOutputsByBlockId(group) - const accumulatedData: RowData = {} - const blockErrors: Record = {} - const writeCtx = { - tableId: cellContext.tableId, - rowId: cellContext.rowId, - workspaceId: cellContext.workspaceId, - groupId: cellContext.groupId, - executionId: parentExecutionId, - requestId: `wfgrp-resume-${parentExecutionId}`, - } - let writeChain: Promise = Promise.resolve() - let terminalWritten = false - - cellOnBlockComplete = async (blockId, output) => { - const outputs = outputsByBlockId.get(blockId) - if (!outputs) return - const blockResult = - output && typeof output === 'object' && 'output' in (output as object) - ? (output as { output: unknown }).output - : output - const errorMessage = - blockResult && - typeof blockResult === 'object' && - typeof (blockResult as { error?: unknown }).error === 'string' - ? (blockResult as { error: string }).error - : null - if (errorMessage) { - blockErrors[blockId] = errorMessage - } else { - for (const out of outputs) { - const plucked = pluckByPath(blockResult, out.path) - if (plucked === undefined) continue - accumulatedData[out.columnName] = plucked as RowData[string] - } - } - const dataSnapshot: RowData = { ...accumulatedData } - const blockErrorsSnapshot = { ...blockErrors } - writeChain = writeChain - .then(async () => { - if (terminalWritten) return - const partial: RowExecutionMetadata = { - status: 'running', - executionId: parentExecutionId, - jobId: null, - workflowId: cellContext.workflowId, - error: null, - blockErrors: blockErrorsSnapshot, - } - await writeWorkflowGroupState(writeCtx, { - executionState: partial, - dataPatch: dataSnapshot, - }) - }) - .catch((err) => { - logger.warn( - `Resume per-block partial write failed (table=${cellContext.tableId} row=${cellContext.rowId} group=${cellContext.groupId}):`, - err - ) - }) - } - - writeCellTerminal = async (status, error) => { - terminalWritten = true - await writeChain.catch(() => {}) - // Paused → keep `pending` + sentinel jobId so eligibility predicates - // continue treating the row as in-flight while we wait on another - // pause. Mirrors the initial cell-task pause branch. - const terminal: RowExecutionMetadata = - status === 'paused' - ? { - status: 'pending', - executionId: parentExecutionId, - jobId: `paused-${parentExecutionId}`, - workflowId: cellContext.workflowId, - error: null, - blockErrors, - } - : { - status, - executionId: parentExecutionId, - jobId: null, - workflowId: cellContext.workflowId, - error, - runningBlockIds: [], - blockErrors, - } - await writeWorkflowGroupState(writeCtx, { - executionState: terminal, - dataPatch: accumulatedData, - }) - } - } else { - logger.warn( - 'Cell context found but table or group missing — falling back to plain resume', - { - parentExecutionId, - tableId: cellContext.tableId, - groupId: cellContext.groupId, - } - ) + // No cell context → plain resume, no lock, no cascade continuation. + if (!cellContext || !writers) { + const result = await PauseResumeManager.startResumeExecution({ + resumeEntryId: payload.resumeEntryId, + resumeExecutionId: payload.resumeExecutionId, + pausedExecution, + contextId: payload.contextId, + resumeInput: payload.resumeInput, + userId: payload.userId, + }) + logger.info('Background resume execution completed', { + resumeExecutionId, + workflowId, + success: result.success, + status: result.status, + }) + return { + success: result.success, + workflowId, + executionId: resumeExecutionId, + parentExecutionId, + status: result.status, + output: result.output, + executedAt: new Date().toISOString(), } } - const result = await PauseResumeManager.startResumeExecution({ - resumeEntryId: payload.resumeEntryId, - resumeExecutionId: payload.resumeExecutionId, - pausedExecution, - contextId: payload.contextId, - resumeInput: payload.resumeInput, - userId: payload.userId, - ...(cellOnBlockComplete ? { onBlockComplete: cellOnBlockComplete } : {}), - }) - - if (writeCellTerminal) { - if (result.status === 'paused') { - await writeCellTerminal('paused', null) - } else if (result.success) { - await writeCellTerminal('completed', null) - } else { - await writeCellTerminal('error', result.error ?? 'Workflow execution failed') + // Cell-context path: hold the row's cascade lock for the resume + any + // downstream cascade continuation. On lock contention, fall through to + // resume-only (the lock holder will pick up the resumed group's + // completion on its next eligibility scan). + const outcome = await withCascadeLock( + cellContext.tableId, + cellContext.rowId, + parentExecutionId, + async () => { + const result = await runResumeAndCellTerminal(payload, pausedExecution, writers) + if (result.status === 'paused') return result + await continueCascadeAfterResume(cellContext) + return result } + ) + + let result + if (outcome.status === 'contended') { + logger.info( + `Resume cascade lock held — writing resumed group only (table=${cellContext.tableId} row=${cellContext.rowId} executionId=${parentExecutionId})` + ) + result = await runResumeAndCellTerminal(payload, pausedExecution, writers) + } else { + result = outcome.result } logger.info('Background resume execution completed', { @@ -209,6 +127,192 @@ export async function executeResumeJob(payload: ResumeExecutionPayload) { } } +type CellWriters = { + cellOnBlockComplete: (blockId: string, output: unknown) => Promise + writeCellTerminal: ( + status: 'completed' | 'error' | 'paused', + error: string | null + ) => Promise +} + +async function buildResumeCellWriters( + cellContext: { + tableId: string + rowId: string + workspaceId: string + groupId: string + workflowId: string + }, + parentExecutionId: string +): Promise { + const { getTableById } = await import('@/lib/table/service') + const { writeWorkflowGroupState, buildOutputsByBlockId } = await import('@/lib/table/cell-write') + const { pluckByPath } = await import('@/lib/table/pluck') + + const table = await getTableById(cellContext.tableId) + const group = table?.schema.workflowGroups?.find((g) => g.id === cellContext.groupId) + if (!group) { + logger.warn('Cell context found but table or group missing — falling back to plain resume', { + parentExecutionId, + tableId: cellContext.tableId, + groupId: cellContext.groupId, + }) + return null + } + + const outputsByBlockId = buildOutputsByBlockId(group) + const accumulatedData: RowData = {} + const blockErrors: Record = {} + const writeCtx = { + tableId: cellContext.tableId, + rowId: cellContext.rowId, + workspaceId: cellContext.workspaceId, + groupId: cellContext.groupId, + executionId: parentExecutionId, + requestId: `wfgrp-resume-${parentExecutionId}`, + } + let writeChain: Promise = Promise.resolve() + let terminalWritten = false + + const cellOnBlockComplete = async (blockId: string, output: unknown) => { + const outputs = outputsByBlockId.get(blockId) + if (!outputs) return + const blockResult = + output && typeof output === 'object' && 'output' in (output as object) + ? (output as { output: unknown }).output + : output + const errorMessage = + blockResult && + typeof blockResult === 'object' && + typeof (blockResult as { error?: unknown }).error === 'string' + ? (blockResult as { error: string }).error + : null + if (errorMessage) { + blockErrors[blockId] = errorMessage + } else { + for (const out of outputs) { + const plucked = pluckByPath(blockResult, out.path) + if (plucked === undefined) continue + accumulatedData[out.columnName] = plucked as RowData[string] + } + } + const dataSnapshot: RowData = { ...accumulatedData } + const blockErrorsSnapshot = { ...blockErrors } + writeChain = writeChain + .then(async () => { + if (terminalWritten) return + const partial: RowExecutionMetadata = { + status: 'running', + executionId: parentExecutionId, + jobId: null, + workflowId: cellContext.workflowId, + error: null, + blockErrors: blockErrorsSnapshot, + } + await writeWorkflowGroupState(writeCtx, { + executionState: partial, + dataPatch: dataSnapshot, + }) + }) + .catch((err) => { + logger.warn( + `Resume per-block partial write failed (table=${cellContext.tableId} row=${cellContext.rowId} group=${cellContext.groupId}):`, + err + ) + }) + } + + const writeCellTerminal = async ( + status: 'completed' | 'error' | 'paused', + error: string | null + ) => { + terminalWritten = true + await writeChain.catch(() => {}) + // Paused → keep `pending` + sentinel jobId so eligibility predicates + // continue treating the row as in-flight while we wait on another + // pause. Mirrors the initial cell-task pause branch. + const terminal: RowExecutionMetadata = + status === 'paused' + ? { + status: 'pending', + executionId: parentExecutionId, + jobId: `paused-${parentExecutionId}`, + workflowId: cellContext.workflowId, + error: null, + blockErrors, + } + : { + status, + executionId: parentExecutionId, + jobId: null, + workflowId: cellContext.workflowId, + error, + runningBlockIds: [], + blockErrors, + } + await writeWorkflowGroupState(writeCtx, { + executionState: terminal, + dataPatch: accumulatedData, + }) + } + + return { cellOnBlockComplete, writeCellTerminal } +} + +async function runResumeAndCellTerminal( + payload: ResumeExecutionPayload, + pausedExecution: Awaited>, + writers: CellWriters +): Promise>> { + if (!pausedExecution) throw new Error('Paused execution missing — already nulled by caller') + const result = await PauseResumeManager.startResumeExecution({ + resumeEntryId: payload.resumeEntryId, + resumeExecutionId: payload.resumeExecutionId, + pausedExecution, + contextId: payload.contextId, + resumeInput: payload.resumeInput, + userId: payload.userId, + onBlockComplete: writers.cellOnBlockComplete, + }) + + if (result.status === 'paused') { + await writers.writeCellTerminal('paused', null) + } else if (result.success) { + await writers.writeCellTerminal('completed', null) + } else { + await writers.writeCellTerminal('error', result.error ?? 'Workflow execution failed') + } + + return result +} + +async function continueCascadeAfterResume(cellContext: { + tableId: string + rowId: string + workspaceId: string + groupId: string +}): Promise { + const { getTableById, getRowById } = await import('@/lib/table/service') + const { pickNextEligibleGroupForRow } = await import('@/lib/table/workflow-columns') + const { runRowCascadeLoop } = await import('@/background/workflow-column-execution') + + const freshTable = await getTableById(cellContext.tableId) + if (!freshTable) return + const freshRow = await getRowById(cellContext.tableId, cellContext.rowId, cellContext.workspaceId) + if (!freshRow) return + const next = pickNextEligibleGroupForRow(freshTable, freshRow, cellContext.groupId) + if (!next) return + await runRowCascadeLoop({ + tableId: cellContext.tableId, + tableName: freshTable.name, + rowId: cellContext.rowId, + workspaceId: cellContext.workspaceId, + groupId: next.id, + workflowId: next.workflowId, + executionId: generateId(), + }) +} + export const resumeExecutionTask = task({ id: 'resume-execution', machine: 'medium-1x', diff --git a/apps/sim/background/table-run-dispatcher.ts b/apps/sim/background/table-run-dispatcher.ts new file mode 100644 index 00000000000..441348b8acb --- /dev/null +++ b/apps/sim/background/table-run-dispatcher.ts @@ -0,0 +1,37 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { task } from '@trigger.dev/sdk' +import { runDispatcherToCompletion } from '@/lib/table/dispatcher' + +const logger = createLogger('TableRunDispatcherTask') + +export interface TableRunDispatcherPayload { + dispatchId: string +} + +/** + * Trigger.dev wrapper around `dispatcherStep`. One task run holds the + * dispatcher loop for the dispatch's entire lifetime — each iteration + * processes a window of cells via `batchTriggerAndWait`, which checkpoints + * the parent via CRIU during the wait so we don't pay compute while cells + * execute. The cursor is persisted in DB; if this run crashes, trigger.dev + * retries and the next attempt resumes from the persisted cursor. + */ +export const tableRunDispatcherTask = task({ + id: 'table-run-dispatcher', + machine: 'small-1x', + retry: { maxAttempts: 3 }, + queue: { + name: 'table-run-dispatcher', + concurrencyLimit: 8, + }, + run: async (payload: TableRunDispatcherPayload) => { + const { dispatchId } = payload + try { + await runDispatcherToCompletion(dispatchId) + } catch (err) { + logger.error(`[${dispatchId}] dispatcher loop failed`, { error: toError(err).message }) + throw err + } + }, +}) diff --git a/apps/sim/background/workflow-column-execution.ts b/apps/sim/background/workflow-column-execution.ts index 7c4b977d537..9c62ee21cae 100644 --- a/apps/sim/background/workflow-column-execution.ts +++ b/apps/sim/background/workflow-column-execution.ts @@ -2,38 +2,108 @@ import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger, runWithRequestContext } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' import { task } from '@trigger.dev/sdk' import { eq } from 'drizzle-orm' -import type { RowData, RowExecutionMetadata } from '@/lib/table/types' +import { withCascadeLock } from '@/lib/table/cascade-lock' +import type { + RowData, + RowExecutionMetadata, + TableDefinition, + WorkflowGroup, +} from '@/lib/table/types' +import type { WorkflowGroupCellPayload } from '@/lib/table/workflow-columns' -const logger = createLogger('TriggerWorkflowGroupCell') +export type { WorkflowGroupCellPayload } -export type WorkflowGroupCellPayload = { - tableId: string - tableName: string - rowId: string - groupId: string - workflowId: string - workspaceId: string - /** Sim-side correlation id used as `wfgrp-${executionId}` in logs/requestId. */ - executionId: string -} +const logger = createLogger('TriggerWorkflowGroupCell') -/** - * Background workflow-group cell execution. Runs in a trigger.dev worker; - * writes plain primitives into `row.data[output.columnName]` as picked - * blocks complete, and execution state into `row.executions[groupId]`. - * Cancellation is authoritative via `cancelWorkflowGroupRuns`. - */ +/** Cell-task entrypoint. Holds a per-row cascade lock so only one worker + * advances a given row at a time; bails on contention. The held lock heart- + * beats every 10s so a crashed pod releases within ~30s. */ export async function executeWorkflowGroupCellJob( payload: WorkflowGroupCellPayload, signal?: AbortSignal ) { + const { tableId, rowId, executionId } = payload + const outcome = await withCascadeLock(tableId, rowId, executionId, () => + runRowCascadeLoop(payload, signal) + ) + if (outcome.status === 'contended') { + logger.info( + `Cascade lock held — bailing (table=${tableId} row=${rowId} executionId=${executionId})` + ) + } +} + +/** Re-fetches the table schema each iteration so groups added DURING the + * cascade become visible to the eligibility check. The resume worker must + * already hold the row's cascade lock before calling. */ +export async function runRowCascadeLoop( + payload: WorkflowGroupCellPayload, + signal?: AbortSignal +): Promise { + const { tableId, rowId, workspaceId } = payload + const { getTableById, getRowById } = await import('@/lib/table/service') + const { pickNextEligibleGroupForRow } = await import('@/lib/table/workflow-columns') + + let currentGroupId = payload.groupId + let currentWorkflowId = payload.workflowId + // Fresh executionId per iteration: SQL guard rejects writes whose id ≠ + // row.executions[gid].executionId, so we need a new claim per group. + let currentExecutionId = payload.executionId + + while (true) { + if (signal?.aborted) break + + const freshTable = await getTableById(tableId) + if (!freshTable) { + logger.warn(`Table ${tableId} vanished mid-cascade`) + break + } + const currentGroup = freshTable.schema.workflowGroups?.find((g) => g.id === currentGroupId) + if (!currentGroup) { + logger.warn(`Group ${currentGroupId} no longer exists on table ${tableId}`) + break + } + + const result = await runWorkflowAndWriteTerminal( + { + ...payload, + groupId: currentGroupId, + workflowId: currentWorkflowId, + executionId: currentExecutionId, + }, + signal, + freshTable, + currentGroup + ) + + if (result === 'paused') break + + const freshRow = await getRowById(tableId, rowId, workspaceId) + if (!freshRow) break + const next = pickNextEligibleGroupForRow(freshTable, freshRow, currentGroupId) + if (!next) break + currentGroupId = next.id + currentWorkflowId = next.workflowId + currentExecutionId = generateId() + } +} + +/** Returns `'paused'` to signal the cascade loop must exit (resume worker + * takes over). `'completed' | 'error'` keep the loop running. */ +async function runWorkflowAndWriteTerminal( + payload: WorkflowGroupCellPayload, + signal: AbortSignal | undefined, + table: TableDefinition, + group: WorkflowGroup +): Promise<'completed' | 'error' | 'paused'> { const { tableId, tableName, rowId, groupId, workflowId, workspaceId, executionId } = payload const requestId = `wfgrp-${executionId}` return runWithRequestContext({ requestId }, async () => { - const { getTableById, getRowById, updateRow } = await import('@/lib/table/service') + const { getRowById } = await import('@/lib/table/service') const { executeWorkflow } = await import('@/lib/workflows/executor/execute-workflow') const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils') const { writeWorkflowGroupState, markWorkflowGroupPickedUp, buildOutputsByBlockId } = @@ -44,36 +114,11 @@ export async function executeWorkflowGroupCellJob( const writeState = (executionState: RowExecutionMetadata, dataPatch?: RowData) => writeWorkflowGroupState(cellCtx, { executionState, dataPatch }) - // Hoisted out of the try so the catch block can drain pending writes and - // surface partial errors in the terminal-error state. const blockErrors: Record = {} let writeChain: Promise = Promise.resolve() - // Set right before the terminal `completed`/`error` write fires. The - // executor fires `onBlockComplete` callbacks fire-and-forget, so some can - // still be in the microtask queue after `executeWorkflow` resolves; they - // would otherwise enqueue a `running` partial-write that lands after the - // terminal state and clobber it. Once this flag is set, `schedulePartialWrite` - // becomes a no-op. let terminalWritten = false try { - const table = await getTableById(tableId) - if (!table) { - logger.warn(`Table ${tableId} vanished before execution`) - return - } - const group = (table.schema.workflowGroups ?? []).find((g) => g.id === groupId) - if (!group) { - await writeState({ - status: 'error', - executionId, - jobId: null, - workflowId, - error: `Workflow group ${groupId} no longer exists on this table`, - }) - return - } - const [workflowRecord] = await db .select() .from(workflowTable) @@ -88,7 +133,7 @@ export async function executeWorkflowGroupCellJob( workflowId, error: 'Workflow not found', }) - return + return 'error' } const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) @@ -103,24 +148,22 @@ export async function executeWorkflowGroupCellJob( workflowId, error: 'Workflow is missing a Start trigger', }) - return + return 'error' } const row = await getRowById(tableId, rowId, workspaceId) if (!row) { logger.warn(`Row ${rowId} vanished before execution`) - return + return 'error' } - // Flip `queued` → `running` to signal the worker has actually started. - // Bail out if the cancel-sticky guard rejects the write (a stop click - // landed between enqueue and pickup). - const queuedExec = row.executions?.[groupId] as RowExecutionMetadata | undefined + // SQL guard rejects if a stop click stamped `cancelled` between enqueue + // and pickup. const pickedUp = await markWorkflowGroupPickedUp(cellCtx, { workflowId, - jobId: queuedExec?.jobId ?? null, + jobId: null, }) - if (pickedUp === 'skipped') return + if (pickedUp === 'skipped') return 'error' // Output columns produced by THIS group are skipped on input — they're // populated by the run we're starting. Other group's outputs ARE @@ -137,8 +180,6 @@ export async function executeWorkflowGroupCellJob( .filter((c) => !ownOutputColumns.has(c.name)) .map((c) => c.name) - // Spread row columns as top-level inputs so Start block fields resolve - // directly by column name; reserved metadata keys win on collision. const input = { ...inputRow, row: inputRow, @@ -156,11 +197,9 @@ export async function executeWorkflowGroupCellJob( const { pluckByPath } = await import('@/lib/table/pluck') const outputsByBlockId = buildOutputsByBlockId(group) - // Local accumulators for the run. const accumulatedData: RowData = {} const runningBlockIds = new Set() - /** Snapshot the current state and append a partial write to the chain. */ const schedulePartialWrite = () => { if (terminalWritten) return const dataSnapshot: RowData = { ...accumulatedData } @@ -169,18 +208,12 @@ export async function executeWorkflowGroupCellJob( writeChain = writeChain .then(async () => { if (signal?.aborted) return - // Re-check inside the chain — a write enqueued before the - // terminal flag flipped should still bail if the chain runs - // after the terminal write. if (terminalWritten) return await writeState( { status: 'running', executionId, - // Stamp the jobId from the current row state — the scheduler - // wrote it before this task started, and we don't want to lose - // it on partial writes. Re-read defensively. - jobId: await readJobId(), + jobId: null, workflowId, error: null, runningBlockIds: runningSnapshot, @@ -197,12 +230,6 @@ export async function executeWorkflowGroupCellJob( }) } - const readJobId = async (): Promise => { - const r = await getRowById(tableId, rowId, workspaceId) - const exec = r?.executions?.[groupId] as RowExecutionMetadata | undefined - return exec?.jobId ?? null - } - const onBlockStart = async (blockId: string): Promise => { if (!outputsByBlockId.has(blockId)) return runningBlockIds.add(blockId) @@ -213,7 +240,6 @@ export async function executeWorkflowGroupCellJob( const outputs = outputsByBlockId.get(blockId) if (!outputs) return - // executor hands us `{ input?, output: NormalizedBlockOutput, executionTime, ... }` const blockResult = output && typeof output === 'object' && 'output' in (output as object) ? (output as { output: unknown }).output @@ -231,9 +257,6 @@ export async function executeWorkflowGroupCellJob( } else { for (const out of outputs) { const plucked = pluckByPath(blockResult, out.path) - // Skip when pluck misses — assigning `undefined` would drop the - // key on JSON serialization, clearing any prior value already - // landed for this column. if (plucked === undefined) continue accumulatedData[out.columnName] = plucked as RowData[string] } @@ -257,10 +280,6 @@ export async function executeWorkflowGroupCellJob( executionMode: 'sync', workflowTriggerType: 'table', triggerBlockId: startBlock.id, - // Always run the live workflow state — table cells track the - // current editor state rather than the most recent deploy, so - // every save lands in the next row run without forcing the user - // to re-deploy. useDraftState: true, abortSignal: signal, onBlockStart, @@ -269,20 +288,10 @@ export async function executeWorkflowGroupCellJob( executionId ) - // Drain queued partial writes before the terminal write so a late - // `running` partial doesn't clobber it. Setting `terminalWritten` - // before draining means any onBlockComplete callbacks still in the - // microtask queue (the executor fires them fire-and-forget) become - // no-ops the moment they try to enqueue. terminalWritten = true await writeChain.catch(() => {}) if (result.status === 'paused') { - // HITL pause: keep the row in `pending` so the renderer surfaces it - // the same way logs do, but stamp a sentinel jobId so the scheduler's - // eligibility predicate keeps treating the row as in-flight (no - // re-enqueue while we wait on a human). Resume worker rewrites this - // back to `completed`/`error` once the pause clears. await writeState( { status: 'pending', @@ -304,7 +313,7 @@ export async function executeWorkflowGroupCellJob( workflowId, workspaceId, }) - return + return 'paused' } await writeState( @@ -319,16 +328,13 @@ export async function executeWorkflowGroupCellJob( }, accumulatedData ) + return result.success ? 'completed' : 'error' } catch (err) { const message = toError(err).message logger.error( `Workflow group cell execution failed (table=${tableId} row=${rowId} group=${groupId})`, { error: message, executionId } ) - // Drain queued partial writes before the terminal error write so a late - // `running` partial doesn't clobber it — same reason as the success - // path above. Reset `runningBlockIds`/`blockErrors` explicitly so the - // renderer sees a clean terminal state (otherwise stale spinners stay). terminalWritten = true await writeChain.catch(() => {}) try { @@ -344,6 +350,7 @@ export async function executeWorkflowGroupCellJob( } catch (writeErr) { logger.error('Also failed to write error state', { error: toError(writeErr).message }) } + return 'error' } }) } diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 9d1f6993cdb..90ba8777a7b 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -19,6 +19,7 @@ import { isValidationError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import type { ContractJsonResponse } from '@/lib/api/contracts' import { + type ActiveDispatch, type AddWorkflowGroupBodyInput, addTableColumnContract, addWorkflowGroupContract, @@ -38,6 +39,7 @@ import { deleteWorkflowGroupContract, getTableContract, type InsertTableRowBodyInput, + listActiveDispatchesContract, listTableRowsContract, listTablesContract, type RunMode, @@ -69,7 +71,12 @@ import type { WorkflowGroupDependencies, WorkflowGroupOutput, } from '@/lib/table' -import { areOutputsFilled, optimisticallyScheduleNewlyEligibleGroups } from '@/lib/table/deps' +import { + areGroupDepsSatisfied, + areOutputsFilled, + isExecInFlight, + optimisticallyScheduleNewlyEligibleGroups, +} from '@/lib/table/deps' const logger = createLogger('TableQueries') @@ -86,6 +93,8 @@ export const tableKeys = { infiniteRows: (tableId: string, paramsKey: string) => [...tableKeys.rowsRoot(tableId), 'infinite', paramsKey] as const, rowWrites: (tableId: string) => [...tableKeys.rowsRoot(tableId), 'write'] as const, + activeDispatches: (tableId: string) => + [...tableKeys.detail(tableId), 'active-dispatches'] as const, } type TableRowsParams = Omit & @@ -153,10 +162,6 @@ async function fetchTableRows({ return { rows, totalCount } } -function invalidateRowData(queryClient: ReturnType, tableId: string) { - queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) }) -} - function invalidateRowCount(queryClient: ReturnType, tableId: string) { queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) }) queryClient.invalidateQueries({ queryKey: tableKeys.detail(tableId) }) @@ -202,6 +207,40 @@ export function useTable(workspaceId: string | undefined, tableId: string | unde }) } +export interface TableRunState { + dispatches: ActiveDispatch[] + runningCellCount: number + runningByRowId: Record +} + +async function fetchTableRunState(tableId: string, signal?: AbortSignal): Promise { + const response = await requestJson(listActiveDispatchesContract, { + params: { tableId }, + signal, + }) + return { + dispatches: response.data.dispatches, + runningCellCount: response.data.runningCellCount, + runningByRowId: response.data.runningByRowId, + } +} + +/** + * Aggregate live state for a table: active dispatches (drives the "about to + * run" overlay), the running-cell count (top-right counter), and per-row + * running counts (per-row badge). Bootstrap snapshot fetched once on mount; + * SSE `kind: 'cell'` and `kind: 'dispatch'` events incrementally update the + * same cache. + */ +export function useTableRunState(tableId: string | undefined) { + return useQuery({ + queryKey: tableKeys.activeDispatches(tableId ?? ''), + queryFn: ({ signal }) => fetchTableRunState(tableId as string, signal), + enabled: Boolean(tableId), + staleTime: 30 * 1000, + }) +} + interface InfiniteTableRowsParams { workspaceId: string tableId: string @@ -409,7 +448,11 @@ export function useCreateTableRow({ workspaceId, tableId }: RowMutationContext) const row = response.data.row if (!row) return - reconcileCreatedRow(queryClient, tableId, row) + const groups = + queryClient.getQueryData(tableKeys.detail(tableId))?.schema + .workflowGroups ?? [] + const stamped = withOptimisticAutoFireExec(groups, row) + reconcileCreatedRow(queryClient, tableId, stamped) }, onError: (error) => { if (isValidationError(error)) return @@ -421,6 +464,19 @@ export function useCreateTableRow({ workspaceId, tableId }: RowMutationContext) }) } +/** + * Pre-stamp `pending` for any auto-fire-eligible workflow groups on a row that + * was just inserted server-side. Mirrors the server's `mode: 'new'` dispatch: + * the server will fire these groups in the background; the optimistic stamp + * shows the user a `queued` badge immediately rather than waiting ~1s for the + * first SSE event. + */ +function withOptimisticAutoFireExec(groups: WorkflowGroup[], row: TableRow): TableRow { + const nextExecutions = optimisticallyScheduleNewlyEligibleGroups(groups, row, {}) + if (!nextExecutions) return row + return { ...row, executions: nextExecutions } +} + /** * Apply a row-level transformation to all cached infinite row queries for this * table. Used for cell edits where positions don't change. @@ -602,11 +658,6 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext) if (isValidationError(error)) return toast.error(error.message, { duration: 5000 }) }, - onSettled: () => { - if (queryClient.isMutating({ mutationKey: tableKeys.rowWrites(tableId) }) === 1) { - invalidateRowData(queryClient, tableId) - } - }, }) } @@ -666,11 +717,6 @@ export function useBatchUpdateTableRows({ workspaceId, tableId }: RowMutationCon if (isValidationError(error)) return toast.error(error.message, { duration: 5000 }) }, - onSettled: () => { - if (queryClient.isMutating({ mutationKey: tableKeys.rowWrites(tableId) }) === 1) { - invalidateRowData(queryClient, tableId) - } - }, }) } @@ -869,13 +915,19 @@ export function useCancelTableRuns({ workspaceId, tableId }: RowMutationContext) const nextExecutions: RowExecutions = { ...executions } for (const gid in executions) { const exec = executions[gid] - if (!isOptimisticInFlight(exec)) continue - // Preserve blockErrors so cells that already errored keep their - // Error rendering after the stop — only cells without a value or - // error should flip to "Cancelled". + if (!isExecInFlight(exec)) continue + if (exec.executionId == null) { + // Optimistic-only or dispatcher-pre-stamp pending — server has not + // claimed the cell yet, so no SSE will arrive to reconcile a + // `cancelled` stamp. Strip the entry instead and let the renderer + // fall through to the cell's prior state (value / empty / etc.). + delete nextExecutions[gid] + rowTouched = true + continue + } nextExecutions[gid] = { status: 'cancelled', - executionId: exec.executionId ?? null, + executionId: exec.executionId, jobId: null, workflowId: exec.workflowId, error: 'Cancelled', @@ -892,6 +944,11 @@ export function useCancelTableRuns({ workspaceId, tableId }: RowMutationContext) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) }) + // Refetch the run-state snapshot — server re-derives runningCellCount + + // runningByRowId from the freshly-updated sidecar via countRunningCells. + // Without this, the counter and row gutter button stay stale until the + // user refetches manually. + queryClient.invalidateQueries({ queryKey: tableKeys.activeDispatches(tableId) }) }, }) } @@ -1251,14 +1308,6 @@ function buildPendingExec( } } -/** Broader sibling of `isExecInFlight` from `lib/table/deps`: treats any - * `pending` (with or without a jobId) as in-flight. The optimistic-patch - * context uses this to avoid re-marking a cell we just flipped optimistically. - * The eligibility predicate uses the stricter version. */ -function isOptimisticInFlight(exec: RowExecutionMetadata | undefined): boolean { - return exec?.status === 'running' || exec?.status === 'queued' || exec?.status === 'pending' -} - /** * The single canonical run mutation. Every UI gesture (single cell, per-row * Play, action-bar Play/Refresh, column-header menu) maps to a `groupIds` + @@ -1292,32 +1341,47 @@ export function useRunColumn({ workspaceId, tableId }: RowMutationContext) { const executions = r.executions ?? {} let changed = false const next: RowExecutions = { ...executions } + const nextData = { ...r.data } for (const groupId of targetGroupIds) { const exec = executions[groupId] as RowExecutionMetadata | undefined - if (isOptimisticInFlight(exec)) continue + if (isExecInFlight(exec)) continue + const group = groupsById.get(groupId) + // Mirror server eligibility: rows with unmet deps are skipped by the + // dispatcher regardless of mode. Stamping pending here would leave + // the cell flashing Queued indefinitely (no SSE event will arrive). + if (group && !areGroupDepsSatisfied(group, r)) continue // Mirror server eligibility for `mode: 'incomplete'`: skip cells whose // outputs are filled, regardless of exec status. A cancelled/error // cell with a leftover value from a prior run was rendering as filled // but flipping to "queued" optimistically here even though the server // would skip it. - if (runMode === 'incomplete') { - const group = groupsById.get(groupId) - if (group && areOutputsFilled(group, r)) continue - } + if (runMode === 'incomplete' && group && areOutputsFilled(group, r)) continue next[groupId] = buildPendingExec(exec) + // Mirror the server-side bulk clear: wipe output values so the cell + // doesn't render the stale completed value behind a pending badge. + // Without this the cell-render path's "value wins" branch keeps + // showing the previous run's output and the Queued/Running pill + // never appears. + if (group) { + for (const o of group.outputs) { + if (o.columnName in nextData) nextData[o.columnName] = null + } + } changed = true } if (!changed) return null - return { ...r, executions: next } + return { ...r, data: nextData, executions: next } }) return { snapshots } }, onError: (_err, _variables, context) => { if (context?.snapshots) restoreCachedWorkflowCells(queryClient, context.snapshots) }, - // No reconciliation here — useTableEventStream is the source of truth for - // post-mutation cache state, and a refetch would race its incremental - // patches. + onSuccess: () => { + // Seed the active-dispatch overlay immediately (insertDispatch ran + // server-side before responding); rows cache stays owned by SSE. + void queryClient.invalidateQueries({ queryKey: tableKeys.activeDispatches(tableId) }) + }, }) } diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index db7ecbb57ad..18f23ae67e3 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -898,11 +898,13 @@ export const runColumnContract = defineRouteContract({ response: { mode: 'json', /** - * `triggered` is `null` when the dispatcher runs in the background — the - * actual count is only known after a fan-out that may be tens of thousands - * of rows, and we don't hold the HTTP response open for that long. + * `dispatchId` is the id of the `table_run_dispatches` row created for + * this run. The dispatcher task picks it up and crawls the table row by + * row; clients receive cell + dispatch events via SSE. Null when + * trigger.dev is disabled — in that mode cells run inline in-process and + * no dispatch row is created. */ - schema: successResponseSchema(z.object({ triggered: z.number().nullable() })), + schema: successResponseSchema(z.object({ dispatchId: z.string().min(1).nullable() })), }, }) @@ -915,6 +917,47 @@ export type RunColumnBodyInput = z.input * builds a run-column payload. Single source of truth for the literal pair. */ export type RunMode = NonNullable +/** + * Active dispatch overlay: rows in the scope ahead of `cursor` render as + * `pending` on refresh, so a long Run-all doesn't lose its queued indicators. + * Returned by `GET /api/table/[tableId]/dispatches`; mirrored client-side via + * `kind: 'dispatch'` SSE events. + */ +export const activeDispatchSchema = z.object({ + id: z.string(), + status: z.enum(['pending', 'dispatching']), + mode: z.enum(['all', 'incomplete', 'new']), + isManualRun: z.boolean(), + cursor: z.number().int(), + scope: z.object({ + groupIds: z.array(z.string()), + rowIds: z.array(z.string()).optional(), + }), +}) + +export const listActiveDispatchesContract = defineRouteContract({ + method: 'GET', + path: '/api/table/[tableId]/dispatches', + params: tableIdParamsSchema, + response: { + mode: 'json', + schema: successResponseSchema( + z.object({ + dispatches: z.array(activeDispatchSchema), + /** Total cells across the table whose `status === 'running'`. The + * client maintains this incrementally via cell SSE events; this + * field is the bootstrap snapshot on mount. */ + runningCellCount: z.number().int().nonnegative(), + /** Map rowId → number of running cells on that row. Drives the + * per-row badge next to the Stop button. */ + runningByRowId: z.record(z.string(), z.number().int().positive()), + }) + ), + }, +}) + +export type ActiveDispatch = z.output + export const tableEventStreamQuerySchema = z.object({ from: z.preprocess((value) => { if (typeof value !== 'string') return 0 diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index 1baee52e9d0..3d20c4f7780 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -529,6 +529,11 @@ export const userTableServerTool: BaseServerTool // doesn't, so the guard never trips here. Defensive narrowing. return { success: false, message: 'Row update was skipped' } } + // Auto-dispatch for user edits is handled inside `updateRow` + // (mode: 'new' for newly-cleared groups + cancel+rerun for in-flight + // downstream groups). Firing a second mode: 'incomplete' dispatch + // here would race with the internal one AND bulk-clear sibling-group + // outputs (mode: 'incomplete' wipes terminal-state cells in scope). return { success: true, @@ -1418,24 +1423,19 @@ export const userTableServerTool: BaseServerTool } const requestId = generateId().slice(0, 8) assertNotAborted() - // Dispatch in the background — large fan-outs (thousands of rows) - // issue sequential trigger.dev calls and would otherwise hold the - // tool span open for minutes, blocking the chat connection. - void runWorkflowColumn({ + const { dispatchId } = await runWorkflowColumn({ tableId: args.tableId, workspaceId, groupIds, mode: runMode, rowIds, requestId, - }).catch((err) => { - logger.error(`[${requestId}] run_column dispatch failed`, err) }) const scopeLabel = rowIds ? `${rowIds.length} row(s) by id` : runMode return { success: true, message: `Started running ${groupIds.length} column(s) (${scopeLabel}). Cells will populate as workflows complete.`, - data: { triggered: null }, + data: { dispatchId }, } } diff --git a/apps/sim/lib/core/async-jobs/backends/database.ts b/apps/sim/lib/core/async-jobs/backends/database.ts index 0350d57aa6c..1770c464c82 100644 --- a/apps/sim/lib/core/async-jobs/backends/database.ts +++ b/apps/sim/lib/core/async-jobs/backends/database.ts @@ -1,7 +1,7 @@ import { asyncJobs, db } from '@sim/db' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' +import { generateShortId } from '@sim/utils/id' import { eq, sql } from 'drizzle-orm' import { type EnqueueOptions, @@ -37,6 +37,14 @@ function rowToJob(row: AsyncJobRow): Job { const inlineAbortControllers = new Map() +/** + * Per-cancel-key abort controllers for the `batchEnqueueAndWait` direct-call + * path. Distinct from `inlineAbortControllers` (which keys by jobId) — this + * map keys by the domain `cancelKey` callers pass in, since the await-blocking + * path skips `async_jobs` entirely and has no jobId to cancel by. + */ +const inlineCancelKeyControllers = new Map() + interface Semaphore { limit: number available: number @@ -77,7 +85,7 @@ export class DatabaseJobQueue implements JobQueueBackend { payload: TPayload, options?: EnqueueOptions ): Promise { - const jobId = options?.jobId ?? `run_${generateId().replace(/-/g, '').slice(0, 20)}` + const jobId = options?.jobId ?? `run_${generateShortId(20)}` const now = new Date() await db @@ -116,7 +124,7 @@ export class DatabaseJobQueue implements JobQueueBackend { if (items.length === 0) return [] const now = new Date() const rows = items.map(({ payload, options }) => ({ - id: `run_${generateId().replace(/-/g, '').slice(0, 20)}`, + id: `run_${generateShortId(20)}`, type, payload: payload as Record, status: JOB_STATUS.PENDING, @@ -148,6 +156,44 @@ export class DatabaseJobQueue implements JobQueueBackend { return rows.map((r) => r.id) } + /** Skips `async_jobs` entirely — ids are returned empty since callers can't + * look up rows that don't exist. Cancel goes through `cancelByKey`. */ + async batchEnqueueAndWait( + type: JobType, + items: Array<{ payload: TPayload; options?: EnqueueOptions }> + ): Promise { + if (items.length === 0) return [] + const tracked: Array<{ key: string; controller: AbortController }> = [] + const runs = items.map((item) => { + const runner = item.options?.runner + if (!runner) return Promise.resolve() + const controller = new AbortController() + const cancelKey = item.options?.cancelKey + if (cancelKey) { + inlineCancelKeyControllers.set(cancelKey, controller) + tracked.push({ key: cancelKey, controller }) + } + return runner(item.payload, controller.signal).catch((err) => { + logger.error(`[${type}] Inline run failed`, { + cancelKey, + error: toError(err).message, + }) + }) + }) + try { + await Promise.all(runs) + } finally { + // Compare-and-delete guards against a re-enqueue under the same key + // racing with our cleanup. + for (const t of tracked) { + if (inlineCancelKeyControllers.get(t.key) === t.controller) { + inlineCancelKeyControllers.delete(t.key) + } + } + } + return items.map(() => '') + } + async getJob(jobId: string): Promise { const [row] = await db.select().from(asyncJobs).where(eq(asyncJobs.id, jobId)).limit(1) @@ -228,6 +274,14 @@ export class DatabaseJobQueue implements JobQueueBackend { logger.debug('Marked job as cancelled (DB queue)', { jobId, abortedInline: aborted }) } + cancelByKey(cancelKey: string): boolean { + const controller = inlineCancelKeyControllers.get(cancelKey) + if (!controller) return false + controller.abort('Cancelled') + inlineCancelKeyControllers.delete(cancelKey) + return true + } + /** * Fire-and-forget IIFE that owns the lifecycle for an inline job: registers * the abort controller (so `cancelJob` can interrupt mid-flight), acquires diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index 97a7428b310..ef0fcaed65f 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { taskContext } from '@trigger.dev/core/v3' import { runs, type TriggerOptions, tasks } from '@trigger.dev/sdk' import { type EnqueueOptions, @@ -103,6 +104,50 @@ export class TriggerDevJobQueue implements JobQueueBackend { return ids } + async batchEnqueueAndWait( + type: JobType, + items: Array<{ payload: TPayload; options?: EnqueueOptions }> + ): Promise { + if (items.length === 0) return [] + // The SDK's checkpoint-and-resume requires task runtime context. The only + // caller (`dispatcherStep` invoked by `tableRunDispatcherTask.run`) is + // always inside a task; check defensively so misuse fails at the boundary + // instead of as a confusing SDK internal error. + if (!taskContext.isInsideTask) { + throw new Error( + 'batchEnqueueAndWait requires trigger.dev task runtime context — call from within a registered task' + ) + } + + const taskId = JOB_TYPE_TO_TASK_ID[type] + if (!taskId) throw new Error(`Unknown job type: ${type}`) + + const batchItems = items.map(({ payload, options }) => { + const enrichedPayload = + options?.metadata && typeof payload === 'object' && payload !== null + ? { ...payload, ...options.metadata } + : payload + const tags = buildTags(options) + const batchItem: { + payload: unknown + options?: { concurrencyKey?: string; tags?: string[] } + } = { payload: enrichedPayload } + const batchOpts: { concurrencyKey?: string; tags?: string[] } = {} + if (options?.concurrencyKey) batchOpts.concurrencyKey = options.concurrencyKey + if (tags.length > 0) batchOpts.tags = tags + if (Object.keys(batchOpts).length > 0) batchItem.options = batchOpts + return batchItem + }) + + const result = await tasks.batchTriggerAndWait(taskId, batchItems) + logger.debug('batchTriggerAndWait completed', { + type, + taskId, + runCount: result.runs.length, + }) + return result.runs.map((r) => r.id) + } + async getJob(jobId: string): Promise { try { const run = await runs.retrieve(jobId) @@ -168,6 +213,13 @@ export class TriggerDevJobQueue implements JobQueueBackend { throw error } } + + cancelByKey(_cancelKey: string): boolean { + // No in-process AbortControllers to abort — trigger.dev runs are cancelled + // by jobId or via tag sweep (see `cancelCellRunsByTags`). Callers that + // need both surfaces should fan out themselves. + return false + } } /** diff --git a/apps/sim/lib/core/async-jobs/config.ts b/apps/sim/lib/core/async-jobs/config.ts index b28f5453ea4..5d32dc6fcd8 100644 --- a/apps/sim/lib/core/async-jobs/config.ts +++ b/apps/sim/lib/core/async-jobs/config.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { taskContext } from '@trigger.dev/core/v3' import type { AsyncBackendType, JobQueueBackend } from '@/lib/core/async-jobs/types' import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' @@ -10,10 +11,15 @@ let cachedInlineBackend: JobQueueBackend | null = null /** * Determines which async backend to use based on environment configuration. - * Follows the fallback chain: trigger.dev → database + * Falls back to the database backend when trigger.dev isn't enabled — except + * when this process IS a trigger.dev worker (`taskContext.isInsideTask`), in + * which case the SDK runtime is available regardless of env vars and we + * always want to enqueue back through trigger.dev. Without this carve-out, a + * worker pod missing `TRIGGER_DEV_ENABLED=true` silently routes cell jobs to + * the database backend that nothing's draining. */ export function getAsyncBackendType(): AsyncBackendType { - if (isTriggerDevEnabled) { + if (isTriggerDevEnabled || taskContext.isInsideTask) { return 'trigger-dev' } diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index e1d2c411313..3acb9927c25 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -96,6 +96,14 @@ export interface EnqueueOptions { * payload and an `AbortSignal` driven by `cancelJob`. */ runner?: (payload: TPayload, signal: AbortSignal) => Promise + /** + * Stable identity for cancellation lookups on the database backend's + * `batchEnqueueAndWait` path (which skips `async_jobs` entirely, so there + * is no jobId to cancel by). Lets callers map a domain identity (e.g. + * `tableId:rowId:groupId`) to the in-flight `AbortController`. Ignored + * by trigger.dev — runs there are cancelled by tag or jobId. + */ + cancelKey?: string } /** @@ -118,6 +126,28 @@ export interface JobQueueBackend { items: Array<{ payload: TPayload; options?: EnqueueOptions }> ): Promise + /** + * Enqueue a batch and block until every job has reached a terminal state + * (completed, failed, or cancelled). The caller — typically a dispatcher + * walking work in windows — uses this to gate window N+1 on window N's + * completion. + * + * Backend implementations: + * - Trigger.dev: wraps `tasks.batchTriggerAndWait`. MUST be called from + * inside a registered trigger.dev task (the SDK's checkpoint-and-resume + * requires task runtime context). Backends guard with + * `taskContext.isInsideTask` and throw a clear error otherwise. + * - Database (in-process): bypasses `async_jobs` entirely. Since the + * caller is awaiting in-process, the row would serve no live purpose + * (no cross-process recovery, no by-id lookup, no semaphore needed — + * window size IS the concurrency cap). Calls the runner directly via + * `Promise.all` and resolves on the runner's exit. + */ + batchEnqueueAndWait( + type: JobType, + items: Array<{ payload: TPayload; options?: EnqueueOptions }> + ): Promise + /** * Get a job by ID */ @@ -144,6 +174,15 @@ export interface JobQueueBackend { * should resolve quietly so callers can drive cancel from possibly-stale state. */ cancelJob(jobId: string): Promise + + /** + * Cancel an in-flight job by its `cancelKey` (the domain identity callers + * stamped on enqueue via `EnqueueOptions.cancelKey`). Used by + * `batchEnqueueAndWait` paths that skip per-job ids; the trigger.dev + * backend has no in-process AbortControllers to abort and returns `false`. + * Returns `true` if a matching controller was found and aborted. + */ + cancelByKey(cancelKey: string): boolean } export type AsyncBackendType = 'trigger-dev' | 'database' diff --git a/apps/sim/lib/table/cascade-lock.ts b/apps/sim/lib/table/cascade-lock.ts new file mode 100644 index 00000000000..cfdb4702c5f --- /dev/null +++ b/apps/sim/lib/table/cascade-lock.ts @@ -0,0 +1,61 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { acquireLock, extendLock, releaseLock } from '@/lib/core/config/redis' + +const logger = createLogger('TableCascadeLock') + +/** Lock TTL. Crashed pods release within this many seconds. */ +const LOCK_TTL_SECONDS = 30 +/** Heartbeat cadence. ~3x within TTL — tolerates two missed beats. */ +const HEARTBEAT_INTERVAL_MS = 10_000 + +/** Single source of truth for the cascade-lock key shape. The lock arbitrates + * ownership of a row's full workflow-group cascade — only the owner advances + * the row through its eligible groups. */ +export function cascadeLockKey(tableId: string, rowId: string): string { + return `table:cascade:${tableId}:${rowId}` +} + +/** + * Run `fn` while holding the row's cascade lock, with a heartbeat extending + * the TTL every 10s so a crashed pod releases the lock in ≤30s. `ownerId` + * must be unique per holder (typically the cell-task's `executionId`) so + * `releaseLock` does compare-and-delete and can't accidentally drop another + * owner's lock. + * + * Returns `'acquired'` after `fn` resolves, or `'contended'` if another + * task already holds the lock — `fn` is NOT invoked in that case. The + * caller decides what to do on contention (cell-task bails; resume worker + * still writes the resumed-group's terminal state but skips the cascade). + * + * NOTE: when Redis is unavailable, `acquireLock` returns `true` as a + * single-replica fallback — concurrent cell-tasks would all "acquire" and + * run in parallel. The cell-write SQL guard mitigates double-writes but + * doesn't prevent duplicate workflow executions. + */ +export async function withCascadeLock( + tableId: string, + rowId: string, + ownerId: string, + fn: () => Promise +): Promise<{ status: 'acquired'; result: T } | { status: 'contended' }> { + const key = cascadeLockKey(tableId, rowId) + const acquired = await acquireLock(key, ownerId, LOCK_TTL_SECONDS) + if (!acquired) return { status: 'contended' } + + const heartbeat = setInterval(() => { + extendLock(key, ownerId, LOCK_TTL_SECONDS).catch((err) => { + logger.warn(`Heartbeat refresh failed for ${key}`, { error: toError(err).message }) + }) + }, HEARTBEAT_INTERVAL_MS) + + try { + const result = await fn() + return { status: 'acquired', result } + } finally { + clearInterval(heartbeat) + await releaseLock(key, ownerId).catch((err) => { + logger.warn(`Lock release failed for ${key}`, { error: toError(err).message }) + }) + } +} diff --git a/apps/sim/lib/table/cell-write.ts b/apps/sim/lib/table/cell-write.ts index 5e179319f9e..c2655a1e745 100644 --- a/apps/sim/lib/table/cell-write.ts +++ b/apps/sim/lib/table/cell-write.ts @@ -161,7 +161,9 @@ export async function markWorkflowGroupPickedUp( /** Builds the canonical `cancelled` execution state used by every cancel path. * Preserves `blockErrors` from the prior state so errored cells keep * rendering Error after a stop click — only cells that hadn't yet produced - * a value or an error should flip to "Cancelled". */ + * a value or an error should flip to "Cancelled". `cancelledAt` is the + * tombstone the dispatcher reads to skip re-runs of cells the user killed + * mid-cascade. */ export function buildCancelledExecution( prev: Pick ): RowExecutionMetadata { @@ -171,6 +173,7 @@ export function buildCancelledExecution( jobId: null, workflowId: prev.workflowId, error: 'Cancelled', + cancelledAt: new Date().toISOString(), ...(prev.blockErrors ? { blockErrors: prev.blockErrors } : {}), } } diff --git a/apps/sim/lib/table/deps.ts b/apps/sim/lib/table/deps.ts index d9b33f59dbb..9cc19293a3f 100644 --- a/apps/sim/lib/table/deps.ts +++ b/apps/sim/lib/table/deps.ts @@ -10,18 +10,18 @@ import type { RowData, RowExecutionMetadata, RowExecutions, TableRow, WorkflowGr const logger = createLogger('OptimisticCascade') /** - * True when the cell has a worker actively reserved — `queued` / `running`, - * or `pending` after the scheduler stamped a jobId. Single source of truth - * for the "is this exec in flight" classification across the eligibility - * predicate, optimistic patches, status counters, and renderer. `pending` - * without a jobId is the optimistic-flag-only state, not in-flight. + * True when the cell is `pending` / `queued` / `running`. Single source of + * truth for the "is this exec in flight" classification across the + * eligibility predicate, optimistic patches, status counters, and renderer. + * `pending` counts even without a jobId so the row-gutter Stop button is + * available the moment the user clicks Play — the cancel path writes + * `cancelled` authoritatively whether or not a real trigger.dev run exists + * yet, which is correct: cancel means "don't run this." */ export function isExecInFlight(exec: RowExecutionMetadata | undefined): boolean { if (!exec) return false const s = exec.status - if (s === 'queued' || s === 'running') return true - if (s === 'pending' && exec.jobId) return true - return false + return s === 'queued' || s === 'running' || s === 'pending' } /** @@ -75,10 +75,10 @@ export function getUnmetGroupDeps(group: WorkflowGroup, row: TableRow): UnmetDep /** * Optimistic mirror of the server's row-update→scheduler cascade: for every * workflow group whose deps were unmet *before* the patch and are satisfied - * *after*, return a new `executions` map with that group flipped to - * `pending`. The cell renderer treats `pending` as "Queued", which is what - * the user expects to see immediately after they fill in the missing input — - * not a flash of dash before the server's pending write arrives. + * *after*, OR whose dep column was touched by the patch (the server will + * cancel+re-run via `deriveExecClearsForDataPatch` + the in-flight cancel + * orchestration), return a new `executions` map with that group flipped to + * `pending`. The cell renderer treats `pending` as "Queued". * * Returns `null` when nothing changed, so callers can short-circuit. */ @@ -93,6 +93,7 @@ export function optimisticallyScheduleNewlyEligibleGroups( ...beforeRow, data: { ...beforeRow.data, ...patch } as RowData, } + const patchedColumns = new Set(Object.keys(patch)) let next: RowExecutions | null = null let flipped = 0 @@ -108,10 +109,6 @@ export function optimisticallyScheduleNewlyEligibleGroups( } const exec = beforeRow.executions?.[group.id] - if (exec?.status === 'queued' || exec?.status === 'running') { - skipped++ - continue - } if (exec?.status === 'pending' && exec.jobId) { skipped++ continue @@ -121,7 +118,17 @@ export function optimisticallyScheduleNewlyEligibleGroups( const wasSatisfied = areGroupDepsSatisfied(group, beforeRow) const becameSatisfied = !wasSatisfied const isRetryable = exec?.status === 'cancelled' || exec?.status === 'error' - if (!becameSatisfied && !isStaleCompleted && !isRetryable && exec) { + // Dep-column touched: the server clears terminal entries + cancels in- + // flight downstream groups, so optimistically flip to `pending` + // regardless of current exec status (queued/running included — they're + // about to be cancelled and re-run). + const depTouched = (group.dependencies?.columns ?? []).some((d) => patchedColumns.has(d)) + + if (!depTouched && (exec?.status === 'queued' || exec?.status === 'running')) { + skipped++ + continue + } + if (!becameSatisfied && !isStaleCompleted && !isRetryable && !depTouched && exec) { skipped++ continue } diff --git a/apps/sim/lib/table/dispatcher.ts b/apps/sim/lib/table/dispatcher.ts new file mode 100644 index 00000000000..ab91ff67cb4 --- /dev/null +++ b/apps/sim/lib/table/dispatcher.ts @@ -0,0 +1,543 @@ +import { db } from '@sim/db' +import { tableRowExecutions, tableRunDispatches, userTableRows } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, gt, inArray, type SQL, sql } from 'drizzle-orm' +import { getJobQueue } from '@/lib/core/async-jobs/config' +import { writeWorkflowGroupState } from '@/lib/table/cell-write' +import { appendTableEvent } from '@/lib/table/events' +import type { RowExecutionMetadata, RowExecutions, TableRow } from '@/lib/table/types' +import { + buildEnqueueItems, + buildPendingRuns, + TABLE_CONCURRENCY_LIMIT, + toTableRow, + type WorkflowGroupCellPayload, +} from './workflow-columns' + +const logger = createLogger('TableRunDispatcher') + +/** Window size matches the cell-execution concurrency cap so one window + * saturates the pool before the next is loaded — yields a row-major + * scan-line crawl (rows 1-20 finish before 21-40 start). */ +const WINDOW_SIZE = TABLE_CONCURRENCY_LIMIT + +const ACTIVE_DISPATCH_STATUSES = ['pending', 'dispatching'] as const + +export type DispatchStatus = 'pending' | 'dispatching' | 'complete' | 'cancelled' +export type DispatchMode = 'all' | 'incomplete' | 'new' + +export interface DispatchScope { + groupIds: string[] + rowIds?: string[] +} + +export interface DispatchRow { + id: string + tableId: string + workspaceId: string + requestId: string + mode: DispatchMode + scope: DispatchScope + status: DispatchStatus + cursor: number + isManualRun: boolean + requestedAt: Date +} + +export type DispatcherStepResult = 'continue' | 'done' + +/** Eager bulk clear at click time so the user sees every targeted cell go + * blank/Pending instantly — without it, only the rows the dispatcher has + * reached visibly change, and the rest sit on stale data until the cursor + * walks to them. For `mode: 'incomplete'` we skip rows whose outputs are + * already filled, mirroring the eligibility predicate. */ +export async function bulkClearWorkflowGroupCells(input: { + tableId: string + groups: Array<{ id: string; outputs: Array<{ columnName: string }> }> + rowIds?: string[] + mode: DispatchMode +}): Promise { + const { tableId, groups, rowIds, mode } = input + if (groups.length === 0) return + // `'new'` mode targets only rows with no prior attempt — nothing to clear. + // Pre-existing outputs on any other row must not be wiped by an auto-fire. + if (mode === 'new') return + + const outputCols = Array.from(new Set(groups.flatMap((g) => g.outputs.map((o) => o.columnName)))) + const groupIds = groups.map((g) => g.id) + + // Step 1: clear the targeted output columns from `data` on every row in + // scope. Identical chain to the previous JSONB-only path. + let dataExpr: SQL = sql`coalesce(${userTableRows.data}, '{}'::jsonb)` + for (const col of outputCols) dataExpr = sql`(${dataExpr}) - ${col}::text` + + const filters: SQL[] = [eq(userTableRows.tableId, tableId)] + if (rowIds && rowIds.length > 0) { + filters.push(inArray(userTableRows.id, rowIds)) + } + if (mode === 'incomplete') { + // Skip rows where all output columns across all targeted groups already + // have a non-empty value — those are "completed-and-filled" and the + // eligibility predicate would skip them anyway. + const filledChecks = outputCols.map( + (col) => sql`coalesce(${userTableRows.data} ->> ${col}, '') != ''` + ) + const allFilled = filledChecks.reduce((acc, expr) => sql`${acc} AND ${expr}`) + filters.push(sql`NOT (${allFilled})`) + // Also skip rows where ANY targeted group has an in-flight exec — those + // belong to another dispatch and clobbering them would race. Encoded as + // a NOT EXISTS subquery against the sidecar's `(table_id, status)` + // partial index. + filters.push( + sql`NOT EXISTS ( + SELECT 1 FROM ${tableRowExecutions} re + WHERE re.row_id = ${userTableRows.id} + AND re.group_id = ANY(ARRAY[${sql.join( + groupIds.map((gid) => sql`${gid}`), + sql`, ` + )}]::text[]) + AND re.status IN ('queued', 'running', 'pending') + )` + ) + } + + await db.transaction(async (trx) => { + await trx + .update(userTableRows) + .set({ data: dataExpr, updatedAt: new Date() }) + .where(and(...filters)) + + // Step 2: delete the targeted groups' executions for the rows in scope. + // Reuse the same row-scope filter via a subquery. + const execFilters: SQL[] = [ + eq(tableRowExecutions.tableId, tableId), + inArray(tableRowExecutions.groupId, groupIds), + ] + if (rowIds && rowIds.length > 0) { + execFilters.push(inArray(tableRowExecutions.rowId, rowIds)) + } + if (mode === 'incomplete') { + // For `incomplete`, only delete entries that aren't already in-flight + // — terminal states (completed/error/cancelled) get wiped so the + // dispatcher re-enqueues; in-flight entries stay so we don't race + // with their worker. + execFilters.push(sql`${tableRowExecutions.status} NOT IN ('queued', 'running', 'pending')`) + } + await trx.delete(tableRowExecutions).where(and(...execFilters)) + }) +} + +export async function insertDispatch(input: { + tableId: string + workspaceId: string + requestId: string + mode: DispatchMode + scope: DispatchScope + isManualRun: boolean +}): Promise { + const id = `tdsp_${generateId().replace(/-/g, '')}` + await db.insert(tableRunDispatches).values({ + id, + tableId: input.tableId, + workspaceId: input.workspaceId, + requestId: input.requestId, + mode: input.mode, + scope: input.scope, + status: 'pending', + // -1 = "haven't started." First window's filter `position > -1` matches + // position 0; subsequent iterations advance to `lastPosition` which then + // correctly excludes already-processed rows. + cursor: -1, + isManualRun: input.isManualRun, + }) + return id +} + +/** Read every dispatch on a table whose status is still `pending` or + * `dispatching`. Drives the client-side "about to run" overlay: rows in an + * active dispatch's scope ahead of its cursor are rendered as queued even + * before the dispatcher has reached them, so refresh during a long Run-all + * doesn't lose the queued indicators. */ +/** Counts in-flight cells (queued / running / pending) across the entire + * table — the authoritative source for the "X running" badge and the per-row + * gutter Run/Stop button. All three statuses are user-cancellable, so the + * gutter must surface Stop whenever any of them are present (else clicking + * Play during the queued window would re-run an already-queued cell). + * Hits the `(table_id, status)` partial index on table_row_executions. */ +export async function countRunningCells( + tableId: string +): Promise<{ total: number; byRowId: Record }> { + const rows = await db + .select({ + rowId: tableRowExecutions.rowId, + runningCount: sql`count(*)::int`, + }) + .from(tableRowExecutions) + .where( + and( + eq(tableRowExecutions.tableId, tableId), + inArray(tableRowExecutions.status, ['queued', 'running', 'pending']) + ) + ) + .groupBy(tableRowExecutions.rowId) + let total = 0 + const byRowId: Record = {} + for (const r of rows) { + if (r.runningCount > 0) { + byRowId[r.rowId] = r.runningCount + total += r.runningCount + } + } + return { total, byRowId } +} + +export async function listActiveDispatches(tableId: string): Promise { + const rows = await db + .select() + .from(tableRunDispatches) + .where( + and( + eq(tableRunDispatches.tableId, tableId), + inArray(tableRunDispatches.status, [...ACTIVE_DISPATCH_STATUSES]) + ) + ) + return rows.map((row) => ({ + id: row.id, + tableId: row.tableId, + workspaceId: row.workspaceId, + requestId: row.requestId, + mode: row.mode as DispatchMode, + scope: row.scope as DispatchScope, + status: row.status as DispatchStatus, + cursor: row.cursor, + isManualRun: row.isManualRun, + requestedAt: row.requestedAt, + })) +} + +export async function readDispatch(dispatchId: string): Promise { + const [row] = await db + .select() + .from(tableRunDispatches) + .where(eq(tableRunDispatches.id, dispatchId)) + .limit(1) + if (!row) return null + return { + id: row.id, + tableId: row.tableId, + workspaceId: row.workspaceId, + requestId: row.requestId, + mode: row.mode as DispatchMode, + scope: row.scope as DispatchScope, + status: row.status as DispatchStatus, + cursor: row.cursor, + isManualRun: row.isManualRun, + requestedAt: row.requestedAt, + } +} + +/** Drive `dispatcherStep` to completion. Shared between the trigger.dev task + * wrapper (`tableRunDispatcherTask`) and the in-process inline path so both + * runtimes use identical loop semantics + error logging. */ +export async function runDispatcherToCompletion(dispatchId: string): Promise { + while ((await dispatcherStep(dispatchId)) === 'continue') {} +} + +/** Run one window of the dispatcher state machine. Caller re-invokes (via the + * trigger.dev task wrapper) until the returned status is `'done'`. */ +export async function dispatcherStep(dispatchId: string): Promise { + const dispatch = await readDispatch(dispatchId) + if (!dispatch) { + logger.warn(`[${dispatchId}] dispatch row missing — aborting`) + return 'done' + } + if (dispatch.status === 'cancelled' || dispatch.status === 'complete') return 'done' + + const { getTableById } = await import('./service') + const table = await getTableById(dispatch.tableId) + if (!table) { + logger.warn(`[${dispatchId}] table ${dispatch.tableId} missing — completing dispatch`) + await markDispatchComplete(dispatchId) + return 'done' + } + + const allGroups = table.schema.workflowGroups ?? [] + const targetGroups = allGroups.filter((g) => dispatch.scope.groupIds.includes(g.id)) + if (targetGroups.length === 0) { + await markDispatchComplete(dispatchId) + return 'done' + } + + // First iteration: just transition pending → dispatching. The bulk clear + // ran synchronously in `runWorkflowColumn` before this task fired, so the + // user already saw the column flip to empty/Pending before any cell + // started enqueueing. + if (dispatch.status === 'pending') { + await db + .update(tableRunDispatches) + .set({ status: 'dispatching' }) + .where(eq(tableRunDispatches.id, dispatchId)) + } + + const filters = [ + eq(userTableRows.tableId, dispatch.tableId), + gt(userTableRows.position, dispatch.cursor), + ] + if (dispatch.scope.rowIds && dispatch.scope.rowIds.length > 0) { + filters.push(inArray(userTableRows.id, dispatch.scope.rowIds)) + } + // `'new'` mode targets only rows whose targeted groups haven't been + // attempted. Exclude a row only when EVERY targeted group already has a + // sidecar entry — if any one is missing, the row still has work to do + // and per-group JS filtering in `classifyEligibility` handles the rest. + if (dispatch.mode === 'new' && dispatch.scope.groupIds.length > 0) { + const gids = dispatch.scope.groupIds + filters.push( + sql`NOT EXISTS ( + SELECT 1 FROM ${tableRowExecutions} re + WHERE re.row_id = ${userTableRows.id} + AND re.group_id = ANY(ARRAY[${sql.join( + gids.map((gid) => sql`${gid}`), + sql`, ` + )}]::text[]) + GROUP BY re.row_id + HAVING count(DISTINCT re.group_id) = ${gids.length} + )` + ) + } + + const chunk = await db + .select() + .from(userTableRows) + .where(and(...filters)) + .orderBy(asc(userTableRows.position)) + .limit(WINDOW_SIZE) + + if (chunk.length === 0) { + await markDispatchComplete(dispatchId) + await appendTableEvent({ + kind: 'dispatch', + tableId: dispatch.tableId, + dispatchId, + status: 'complete', + scope: dispatch.scope, + cursor: dispatch.cursor, + mode: dispatch.mode, + isManualRun: dispatch.isManualRun, + }) + return 'done' + } + + // Pre-fetch executions for the chunk so per-row eligibility doesn't fan + // out into one query per row. Returns `Map`. + const chunkRowIds = chunk.map((r) => r.id) + const execRows = await db + .select() + .from(tableRowExecutions) + .where(inArray(tableRowExecutions.rowId, chunkRowIds)) + const executionsByRow = new Map() + for (const r of execRows) { + const existing = executionsByRow.get(r.rowId) ?? {} + const meta: RowExecutionMetadata = { + status: r.status as RowExecutionMetadata['status'], + executionId: r.executionId ?? null, + jobId: r.jobId ?? null, + workflowId: r.workflowId, + error: r.error ?? null, + ...(r.runningBlockIds && r.runningBlockIds.length > 0 + ? { runningBlockIds: r.runningBlockIds } + : {}), + ...(r.blockErrors && Object.keys(r.blockErrors as Record).length > 0 + ? { blockErrors: r.blockErrors as Record } + : {}), + ...(r.cancelledAt ? { cancelledAt: r.cancelledAt.toISOString() } : {}), + } + existing[r.groupId] = meta + executionsByRow.set(r.rowId, existing) + } + + // Strip rows the user cancelled mid-cascade (post-dispatch tombstones) + // before running the shared eligibility filter — `buildPendingRuns` + // doesn't know about the per-dispatch cancel tombstone. + const tombstoneFiltered: TableRow[] = [] + for (const r of chunk) { + const tableRow = toTableRow(r, executionsByRow.get(r.id) ?? {}) + const tombstoned = dispatch.scope.groupIds.some((gid) => { + const exec = tableRow.executions?.[gid] + if (!exec?.cancelledAt) return false + const cancelledAtMs = Date.parse(exec.cancelledAt) + return Number.isFinite(cancelledAtMs) && cancelledAtMs > dispatch.requestedAt.getTime() + }) + if (!tombstoned) tombstoneFiltered.push(tableRow) + } + + const pendingRuns = buildPendingRuns(table, tombstoneFiltered, { + isManualRun: dispatch.isManualRun, + groupIds: dispatch.scope.groupIds, + mode: dispatch.mode, + }) + + // Cursor advances to the last position in this chunk regardless of + // eligibility — otherwise a window full of skipped cells loops forever. + const lastPosition = chunk[chunk.length - 1].position + + if (pendingRuns.length > 0) { + await stampQueuedForBatch(pendingRuns) + + // Backend-agnostic batch dispatch: trigger.dev wraps `batchTriggerAndWait` + // (CRIU-checkpointed wait); database backend calls the cell-task runner + // directly via Promise.all (skips async_jobs since we're awaiting in- + // process anyway). Either way the parent dispatcher blocks until every + // cell in the window terminates — bounds queue depth at WINDOW_SIZE. + const items = await buildEnqueueItems(pendingRuns) + const queue = await getJobQueue() + try { + await queue.batchEnqueueAndWait('workflow-group-cell', items) + } catch (err) { + logger.error(`[${dispatchId}] batch dispatch failed`, { + error: toError(err).message, + }) + // Cursor advances past this window, so flip the un-claimed pre-stamps to + // terminal `error` (+ SSE) — visible, not stuck pending, re-runnable. + const failedAt = new Date() + await Promise.allSettled( + pendingRuns.map(async (p) => { + const updated = await db + .update(tableRowExecutions) + .set({ status: 'error', error: 'Failed to enqueue run', updatedAt: failedAt }) + .where( + and( + eq(tableRowExecutions.rowId, p.rowId), + eq(tableRowExecutions.groupId, p.groupId), + eq(tableRowExecutions.status, 'pending'), + sql`${tableRowExecutions.executionId} IS NULL` + ) + ) + .returning({ rowId: tableRowExecutions.rowId }) + if (updated.length === 0) return + await appendTableEvent({ + kind: 'cell', + tableId: dispatch.tableId, + rowId: p.rowId, + groupId: p.groupId, + status: 'error', + executionId: null, + jobId: null, + error: 'Failed to enqueue run', + }) + }) + ) + } + } + + await Promise.all([ + advanceCursor(dispatchId, lastPosition), + appendTableEvent({ + kind: 'dispatch', + tableId: dispatch.tableId, + dispatchId, + status: 'dispatching', + scope: dispatch.scope, + cursor: lastPosition, + mode: dispatch.mode, + isManualRun: dispatch.isManualRun, + }), + ]) + + return 'continue' +} + +/** Pre-batch stamp: write each targeted cell as `pending` (no executionId) + * before firing the batch so the renderer shows the cell as in-flight + * immediately. The cell-task overwrites with `running` (and its own + * executionId) once it acquires the row's cascade lock — if another + * cell-task already holds the lock, this task bails and the pending stamp + * is later reconciled by whoever owns the cascade. */ +async function stampQueuedForBatch(pendingRuns: WorkflowGroupCellPayload[]): Promise { + await Promise.allSettled( + pendingRuns.map((runOpts) => + writeWorkflowGroupState(runOpts, { + executionState: { + status: 'pending', + executionId: null, + jobId: null, + workflowId: runOpts.workflowId, + error: null, + }, + }) + ) + ) +} + +async function advanceCursor(dispatchId: string, newCursor: number): Promise { + await db + .update(tableRunDispatches) + .set({ cursor: newCursor }) + .where(eq(tableRunDispatches.id, dispatchId)) +} + +async function markDispatchComplete(dispatchId: string): Promise { + await db + .update(tableRunDispatches) + .set({ status: 'complete', completedAt: new Date() }) + .where(eq(tableRunDispatches.id, dispatchId)) +} + +export async function markDispatchCancelled(dispatchId: string): Promise { + await db + .update(tableRunDispatches) + .set({ status: 'cancelled', cancelledAt: new Date() }) + .where( + and( + eq(tableRunDispatches.id, dispatchId), + inArray(tableRunDispatches.status, [...ACTIVE_DISPATCH_STATUSES]) + ) + ) +} + +/** Mark every active dispatch on this table as cancelled. Single atomic + * UPDATE so the dispatcher's next iteration observes the cancel. Returns the + * dispatches that were cancelled so the caller can emit per-dispatch SSE + * events — without those the client's overlay would hang on "queued" until + * the next refresh. */ +export async function markActiveDispatchesCancelled(tableId: string): Promise { + const cancelled = await db + .update(tableRunDispatches) + .set({ status: 'cancelled', cancelledAt: new Date() }) + .where( + and( + eq(tableRunDispatches.tableId, tableId), + inArray(tableRunDispatches.status, [...ACTIVE_DISPATCH_STATUSES]) + ) + ) + .returning() + const dispatches = cancelled.map((row) => ({ + id: row.id, + tableId: row.tableId, + workspaceId: row.workspaceId, + requestId: row.requestId, + mode: row.mode as DispatchMode, + scope: row.scope as DispatchScope, + status: 'cancelled' as DispatchStatus, + cursor: row.cursor, + isManualRun: row.isManualRun, + requestedAt: row.requestedAt, + })) + await Promise.all( + dispatches.map((d) => + appendTableEvent({ + kind: 'dispatch', + tableId: d.tableId, + dispatchId: d.id, + status: 'cancelled', + scope: d.scope, + cursor: d.cursor, + mode: d.mode, + isManualRun: d.isManualRun, + }) + ) + ) + return dispatches +} diff --git a/apps/sim/lib/table/events.ts b/apps/sim/lib/table/events.ts index 64d4fabc562..63c2dac92c7 100644 --- a/apps/sim/lib/table/events.ts +++ b/apps/sim/lib/table/events.ts @@ -68,30 +68,48 @@ function getMetaKey(tableId: string) { export type TableCellStatus = 'pending' | 'queued' | 'running' | 'completed' | 'cancelled' | 'error' -export interface TableEvent { - kind: 'cell' - tableId: string - rowId: string - groupId: string - status: TableCellStatus - executionId: string | null - jobId: string | null - error: string | null - /** - * Present when this transition wrote new output values; absent on - * pure-status transitions (queued, running, cancelled). The publisher - * already has these in hand from the same updateRow call that wrote DB. - */ - outputs?: Record - /** - * Block-level metadata the renderer reads to distinguish "running" (some - * block actively executing) from "pending-upstream" (run started but this - * column's block hasn't fired yet). The worker fills these on partial - * writes; without them the cell stays on the amber Pending pill. - */ - runningBlockIds?: string[] - blockErrors?: Record -} +export type TableDispatchStatus = 'pending' | 'dispatching' | 'complete' | 'cancelled' + +export type TableEvent = + | { + kind: 'cell' + tableId: string + rowId: string + groupId: string + status: TableCellStatus + executionId: string | null + jobId: string | null + error: string | null + /** + * Present when this transition wrote new output values; absent on + * pure-status transitions (queued, running, cancelled). The publisher + * already has these in hand from the same updateRow call that wrote DB. + */ + outputs?: Record + /** + * Block-level metadata the renderer reads to distinguish "running" (some + * block actively executing) from "pending-upstream" (run started but this + * column's block hasn't fired yet). The worker fills these on partial + * writes; without them the cell stays on the amber Pending pill. + */ + runningBlockIds?: string[] + blockErrors?: Record + } + | { + /** Dispatcher status signal emitted by `dispatcherStep` and the cancel + * path. Drives the client-side "about to run" overlay for rows the + * dispatcher hasn't reached yet. `scope` + `cursor` + `mode` + + * `isManualRun` are carried on every transition so the client can + * upsert without refetching the dispatches list. */ + kind: 'dispatch' + tableId: string + dispatchId: string + status: TableDispatchStatus + scope?: { groupIds: string[]; rowIds?: string[] } + cursor?: number + mode?: 'all' | 'incomplete' | 'new' + isManualRun?: boolean + } export interface TableEventEntry { eventId: number diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 693063bfdd4..7b09325195c 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -8,7 +8,12 @@ */ import { db } from '@sim/db' -import { userTableDefinitions, userTableRows, workflowExecutionLogs } from '@sim/db/schema' +import { + tableRowExecutions, + userTableDefinitions, + userTableRows, + workflowExecutionLogs, +} from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' @@ -64,8 +69,8 @@ import { } from './validation' import { assertValidSchema, - scheduleRunsForRows, - scheduleRunsForTable, + cancelWorkflowGroupRuns, + runWorkflowColumn, stripGroupDeps, } from './workflow-columns' @@ -160,6 +165,36 @@ function scaledStatementTimeoutMs( * @param tableId - Table ID to fetch * @returns Table definition or null if not found */ +/** + * Returns `schema` with `columns` sorted by `metadata.columnOrder` (the user- + * editable visible order). Columns missing from `columnOrder` are appended at + * the end in their original (schema-creation) order — covers tables created + * before `columnOrder` existed and any drift from out-of-band column adds. + * + * This makes `schema.columns` the single source of truth for column order on + * the wire. The client doesn't have to join the two arrays itself — every + * consumer (grid, sidebar, copilot, mothership) gets the same ordered list. + */ +function applyColumnOrderToSchema( + schema: TableSchema, + metadata: TableMetadata | null +): TableSchema { + const order = metadata?.columnOrder + if (!order || order.length === 0) return schema + const byName = new Map() + for (const c of schema.columns) byName.set(c.name, c) + const ordered: TableSchema['columns'] = [] + for (const name of order) { + const c = byName.get(name) + if (c) { + ordered.push(c) + byName.delete(name) + } + } + for (const c of byName.values()) ordered.push(c) + return { ...schema, columns: ordered } +} + export async function getTableById( tableId: string, options?: { includeArchived?: boolean } @@ -191,12 +226,13 @@ export async function getTableById( if (results.length === 0) return null const table = results[0] + const metadata = (table.metadata as TableMetadata) ?? null return { id: table.id, name: table.name, description: table.description, - schema: table.schema as TableSchema, - metadata: (table.metadata as TableMetadata) ?? null, + schema: applyColumnOrderToSchema(table.schema as TableSchema, metadata), + metadata, rowCount: table.rowCount, maxRows: table.maxRows, workspaceId: table.workspaceId, @@ -262,20 +298,23 @@ export async function listTables( ) .orderBy(userTableDefinitions.createdAt) - return tables.map((t) => ({ - id: t.id, - name: t.name, - description: t.description, - schema: t.schema as TableSchema, - metadata: (t.metadata as TableMetadata) ?? null, - rowCount: t.rowCount, - maxRows: t.maxRows, - workspaceId: t.workspaceId, - createdBy: t.createdBy, - archivedAt: t.archivedAt, - createdAt: t.createdAt, - updatedAt: t.updatedAt, - })) + return tables.map((t) => { + const metadata = (t.metadata as TableMetadata) ?? null + return { + id: t.id, + name: t.name, + description: t.description, + schema: applyColumnOrderToSchema(t.schema as TableSchema, metadata), + metadata, + rowCount: t.rowCount, + maxRows: t.maxRows, + workspaceId: t.workspaceId, + createdBy: t.createdBy, + archivedAt: t.archivedAt, + createdAt: t.createdAt, + updatedAt: t.updatedAt, + } + }) } /** @@ -648,9 +687,49 @@ export async function updateTableMetadata( ): Promise { const merged: TableMetadata = { ...(existingMetadata ?? {}), ...metadata } + // When `columnOrder` is in the patch, scrub any workflow-group dependency + // that now sits to the right of (or at the same index as) its group's + // leftmost column. Without this, reordering a column could leave a group + // depending on a column it can no longer reach in the dag — the group + // would never fire. + const newOrder = metadata.columnOrder + let nextSchema: TableSchema | null = null + if (Array.isArray(newOrder) && newOrder.length > 0) { + const [tableRow] = await db + .select({ schema: userTableDefinitions.schema }) + .from(userTableDefinitions) + .where(eq(userTableDefinitions.id, tableId)) + .limit(1) + if (tableRow) { + const schema = tableRow.schema as TableSchema + const groups = schema.workflowGroups ?? [] + if (groups.length > 0) { + const positionOf = new Map() + newOrder.forEach((name, i) => positionOf.set(name, i)) + let mutated = false + const nextGroups = groups.map((group) => { + const ownCols = schema.columns.filter((c) => c.workflowGroupId === group.id) + let leftmost = Number.POSITIVE_INFINITY + for (const c of ownCols) { + const idx = positionOf.get(c.name) ?? Number.POSITIVE_INFINITY + if (idx < leftmost) leftmost = idx + } + if (!Number.isFinite(leftmost)) return group + const deps = group.dependencies?.columns ?? [] + const removed = new Set(deps.filter((dep) => (positionOf.get(dep) ?? -1) >= leftmost)) + if (removed.size === 0) return group + const stripped = stripGroupDeps(group, removed) + if (stripped !== group) mutated = true + return stripped + }) + if (mutated) nextSchema = { ...schema, workflowGroups: nextGroups } + } + } + } + await db .update(userTableDefinitions) - .set({ metadata: merged }) + .set(nextSchema ? { metadata: merged, schema: nextSchema } : { metadata: merged }) .where(eq(userTableDefinitions.id, tableId)) return merged @@ -915,7 +994,7 @@ export async function insertRow( const insertedRow: TableRow = { id: row.id, data: row.data as RowData, - executions: (row.executions as RowExecutions) ?? {}, + executions: {}, position: row.position, createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -930,7 +1009,14 @@ export async function insertRow( table.schema, requestId ) - void scheduleRunsForRows(table, [insertedRow]) + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: [insertedRow.id], + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (insertRow) failed:`, err)) return insertedRow } @@ -1041,14 +1127,25 @@ export async function batchInsertRowsWithTx( const result: TableRow[] = insertedRows.map((r) => ({ id: r.id, data: r.data as RowData, - executions: (r.executions as RowExecutions) ?? {}, + executions: {}, position: r.position, createdAt: r.createdAt, updatedAt: r.updatedAt, })) void fireTableTrigger(data.tableId, table.name, 'insert', result, null, table.schema, requestId) - void scheduleRunsForRows(table, result) + // Scope to the newly-inserted row ids so the dispatcher doesn't walk every + // row in the table. After the sidecar migration, all existing rows have + // zero entries → `mode:'new'`'s `NOT EXISTS` filter would otherwise include + // them, dispatching workflows on every row in a populated table. + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: result.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchInsertRows) failed:`, err)) return result } @@ -1322,11 +1419,12 @@ export async function upsertRow( .where(eq(userTableRows.id, matchedRowId)) .returning() + const executions = await loadExecutionsForRow(trx, updatedRow.id) return { row: { id: updatedRow.id, data: updatedRow.data as RowData, - executions: (updatedRow.executions as RowExecutions) ?? {}, + executions, position: updatedRow.position, createdAt: updatedRow.createdAt, updatedAt: updatedRow.updatedAt, @@ -1354,7 +1452,7 @@ export async function upsertRow( row: { id: insertedRow.id, data: insertedRow.data as RowData, - executions: (insertedRow.executions as RowExecutions) ?? {}, + executions: {}, position: insertedRow.position, createdAt: insertedRow.createdAt, updatedAt: insertedRow.updatedAt, @@ -1389,7 +1487,14 @@ export async function upsertRow( requestId ) } - void scheduleRunsForRows(table, [result.row]) + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: [result.row.id], + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (upsertRow) failed:`, err)) return result } @@ -1473,11 +1578,16 @@ export async function queryRows( `[${requestId}] Queried ${rows.length} rows from table ${table.id} (total: ${totalCount})` ) + const executionsByRow = await loadExecutionsByRow( + db, + rows.map((r) => r.id) + ) + return { rows: rows.map((r) => ({ id: r.id, data: r.data as RowData, - executions: (r.executions as RowExecutions) ?? {}, + executions: executionsByRow.get(r.id) ?? {}, position: r.position, createdAt: r.createdAt, updatedAt: r.updatedAt, @@ -1517,10 +1627,11 @@ export async function getRowById( if (results.length === 0) return null const row = results[0] + const executions = await loadExecutionsForRow(db, row.id) return { id: row.id, data: row.data as RowData, - executions: (row.executions as RowExecutions) ?? {}, + executions, position: row.position, createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -1528,36 +1639,94 @@ export async function getRowById( } /** - * When a user edit clears a workflow output column to empty, also clear the - * exec record for that group. Without this, a `cancelled` (or `error`) exec - * sticks on the row even after the user wipes the output, blocking the - * auto-fire reactor (which respects terminal states). Treating the cleared - * cell as "user wants this re-armed" matches the rule that cells are the - * source of truth — we already do this for `completed` via - * `areOutputsFilled` in the eligibility predicate; this extends the same - * behavior to error/cancelled by making the data clear remove the exec. + * Derive automatic clears + cancellation candidates from a row's data patch. + * + * Walks `schema.workflowGroups` left-to-right with a propagating `dirtied` + * column set. For each group whose deps overlap the dirty set, decide to + * clear (terminal exec) or cancel+rerun (in-flight exec), then add the + * group's outputs to the dirty set so later groups in the chain see them + * as dirty too. This models transitive dep chains as a single forward pass — + * editing column A propagates through group 1 (deps on A) to group 2 (deps + * on group 1's output) without explicit DAG traversal. + * + * Returns: + * - `executionsPatch`: caller's patch + nulls for cleared groups (or + * undefined if nothing applied). + * - `inFlightDownstreamGroups`: groups whose dep was dirtied and that are + * currently in-flight. Cancel-and-restart is the caller's job. * - * Returns a merged `executionsPatch` (caller's patch + null for groups whose - * outputs were cleared), or the caller's patch unchanged if nothing applies. + * Assumption: `workflowGroups[]` is in topological order — a group's deps + * may only reference columns to its left (enforced by `workflow-sidebar`'s + * "Run after" picker + the reorder scrub via `stripGroupDeps`). Violating + * this would silently miss the propagation. */ function deriveExecClearsForDataPatch( dataPatch: RowData, schema: TableSchema, + existingExecutions: RowExecutions, callerPatch: Record | undefined -): Record | undefined { +): { + executionsPatch: Record | undefined + inFlightDownstreamGroups: string[] +} { + const dirtied = new Set(Object.keys(dataPatch)) const groupsToClear = new Set() + const inFlightDownstreamGroups: string[] = [] + + // Own-output clears: when the user wipes a workflow output column, drop + // that group's exec entry so the auto-fire reactor re-arms the cell. + // Also flags the cleared output column as dirty so transitive downstream + // groups see it. for (const [columnName, value] of Object.entries(dataPatch)) { const cleared = value === null || value === undefined || value === '' if (!cleared) continue const col = schema.columns.find((c) => c.name === columnName) if (col?.workflowGroupId) groupsToClear.add(col.workflowGroupId) } - if (groupsToClear.size === 0) return callerPatch + + // Left-to-right walk, propagating dirty columns forward. + const groups = schema.workflowGroups ?? [] + for (const group of groups) { + const deps = group.dependencies?.columns ?? [] + const depMatched = deps.some((d) => dirtied.has(d)) + if (!depMatched) continue + + const exec = existingExecutions[group.id] + if (exec) { + const status = exec.status + if (status === 'completed' || status === 'error' || status === 'cancelled') { + groupsToClear.add(group.id) + } else if (status === 'queued' || status === 'running' || status === 'pending') { + inFlightDownstreamGroups.push(group.id) + } + } else { + // No exec entry yet — `mode: 'new'` already covers this group. We + // still propagate the dirty signal forward so later groups in the + // chain see this group's outputs as dirty too. + groupsToClear.add(group.id) + } + + // Propagate: this group is about to be re-computed, so groups whose + // deps reference its output columns are also dirty. + for (const out of group.outputs) dirtied.add(out.columnName) + } + + if (groupsToClear.size === 0) { + return { executionsPatch: callerPatch, inFlightDownstreamGroups } + } const merged: Record = { ...(callerPatch ?? {}) } for (const gid of groupsToClear) { if (!(gid in merged)) merged[gid] = null } - return merged + return { executionsPatch: merged, inFlightDownstreamGroups } +} + +/** Internal: thrown inside `db.transaction` to roll back when the executions + * guard rejects a write. The outer `.catch` translates it into a `null` return. */ +class GuardRejected extends Error { + constructor() { + super('cell-write guard rejected') + } } /** Merges an `executionsPatch` into the row's existing executions blob. */ @@ -1578,50 +1747,172 @@ function applyExecutionsPatch( } /** - * Builds a SQL expression that applies the given `executionsPatch` to the - * row's `executions` jsonb in-place — set keys for non-null values, delete - * keys for `null` values. Returns null when the patch is empty/missing. + * Loads `tableRowExecutions` rows for the given row ids and groups them into + * a `Map` suitable for plugging into `TableRow.executions` + * everywhere callers used to read `userTableRows.executions` JSONB. + */ +async function loadExecutionsByRow( + trx: DbOrTx, + rowIds: Iterable +): Promise> { + const ids = Array.from(new Set(rowIds)) + const result = new Map() + if (ids.length === 0) return result + const rows = await trx + .select() + .from(tableRowExecutions) + .where(inArray(tableRowExecutions.rowId, ids)) + for (const r of rows) { + const existing = result.get(r.rowId) ?? {} + const meta: RowExecutionMetadata = { + status: r.status as RowExecutionMetadata['status'], + executionId: r.executionId ?? null, + jobId: r.jobId ?? null, + workflowId: r.workflowId, + error: r.error ?? null, + ...(r.runningBlockIds && r.runningBlockIds.length > 0 + ? { runningBlockIds: r.runningBlockIds } + : {}), + ...(r.blockErrors && Object.keys(r.blockErrors as Record).length > 0 + ? { blockErrors: r.blockErrors as Record } + : {}), + ...(r.cancelledAt ? { cancelledAt: r.cancelledAt.toISOString() } : {}), + } + existing[r.groupId] = meta + result.set(r.rowId, existing) + } + return result +} + +/** Convenience: load executions for one row, returning `{}` when missing. */ +async function loadExecutionsForRow(trx: DbOrTx, rowId: string): Promise { + const byRow = await loadExecutionsByRow(trx, [rowId]) + return byRow.get(rowId) ?? {} +} + +/** + * Writes a per-group execution patch for one row against the `tableRowExecutions` + * sidecar. Non-null values upsert into the table; nulls delete the entry. When + * `guard` is set, the upsert is gated to: + * - reject if a `cancelled` row for the same execution already exists, and + * - reject if the row exists but is owned by a different executionId + * (with carve-outs for missing rows and null executionIds — the dispatcher's + * pre-batch `pending` stamp leaves executionId unset so the first cell-task + * can claim). * - * Why server-side: read-modify-write on the entire jsonb blob races between - * concurrent writers (e.g., a column edit and a manual-retry stamp), so the - * last writer wins for keys it didn't touch and clobbers other writers' - * exec updates. Patching keys at the SQL level keeps each writer's changes - * atomic per-key. + * Returns `'guard-rejected'` when the guarded group's upsert affected 0 rows + * (callers signal failure to the cell-task path). Returns `'wrote'` otherwise. */ -function buildExecutionsSqlPatch( - patch: Record | undefined -): SQL | null { - if (!patch) return null +async function writeExecutionsPatch( + trx: DbOrTx, + tableId: string, + rowId: string, + patch: Record | undefined, + guard?: { groupId: string; executionId: string } +): Promise<'wrote' | 'guard-rejected'> { + if (!patch) return 'wrote' const entries = Object.entries(patch) - if (entries.length === 0) return null + if (entries.length === 0) return 'wrote' - let expr: SQL = sql`coalesce(${userTableRows.executions}, '{}'::jsonb)` for (const [gid, value] of entries) { if (value === null) { - expr = sql`(${expr}) - ${gid}::text` - } else { - expr = sql`(${expr}) || jsonb_build_object(${gid}::text, ${JSON.stringify(value)}::jsonb)` + await trx + .delete(tableRowExecutions) + .where(and(eq(tableRowExecutions.rowId, rowId), eq(tableRowExecutions.groupId, gid)) as SQL) + continue + } + const insertValues = { + tableId, + rowId, + groupId: gid, + status: value.status, + executionId: value.executionId, + jobId: value.jobId, + workflowId: value.workflowId, + error: value.error, + runningBlockIds: value.runningBlockIds ?? [], + blockErrors: value.blockErrors ?? {}, + cancelledAt: value.cancelledAt ? new Date(value.cancelledAt) : null, + updatedAt: new Date(), + } as const + + const isGuarded = guard && guard.groupId === gid + if (isGuarded) { + // Gate by guard semantics. The original JSONB guard had two AND'd + // clauses; we collapse them onto the upsert's WHERE so a non-matching + // existing row leaves the table untouched and we observe 0 affected. + const guardExecutionId = guard.executionId + const updated = await trx + .insert(tableRowExecutions) + .values(insertValues) + .onConflictDoUpdate({ + target: [tableRowExecutions.rowId, tableRowExecutions.groupId], + set: { + status: insertValues.status, + executionId: insertValues.executionId, + jobId: insertValues.jobId, + workflowId: insertValues.workflowId, + error: insertValues.error, + runningBlockIds: insertValues.runningBlockIds, + blockErrors: insertValues.blockErrors, + cancelledAt: insertValues.cancelledAt, + updatedAt: insertValues.updatedAt, + }, + where: and( + // Reject if this group already shows authoritative `cancelled` for + // the same executionId — a stop click wrote it first. + sql`NOT (${tableRowExecutions.status} = 'cancelled' AND ${tableRowExecutions.executionId} IS NOT DISTINCT FROM ${guardExecutionId})`, + // Stale-worker: the cell's active run has moved on. Carve-outs + // permit a fresh worker to take over when the row's executionId + // is unset (dispatcher's pre-batch `pending` stamp). + sql`(${tableRowExecutions.executionId} IS NULL OR ${tableRowExecutions.executionId} = ${guardExecutionId})` + ) as SQL, + }) + .returning({ rowId: tableRowExecutions.rowId }) + if (updated.length === 0) return 'guard-rejected' + continue } + + await trx + .insert(tableRowExecutions) + .values(insertValues) + .onConflictDoUpdate({ + target: [tableRowExecutions.rowId, tableRowExecutions.groupId], + set: { + status: insertValues.status, + executionId: insertValues.executionId, + jobId: insertValues.jobId, + workflowId: insertValues.workflowId, + error: insertValues.error, + runningBlockIds: insertValues.runningBlockIds, + blockErrors: insertValues.blockErrors, + cancelledAt: insertValues.cancelledAt, + updatedAt: insertValues.updatedAt, + }, + }) } - return expr + + return 'wrote' } /** - * Strips the given workflow group ids from every row's `executions` jsonb on - * a table — used by the column / group delete paths so stale running/queued - * exec records don't linger and inflate counters after the group is gone. - * The caller wraps in their own transaction. + * Strips the given workflow group ids from every row's executions on a table — + * used by the column / group delete paths so stale running/queued exec records + * don't linger and inflate counters after the group is gone. The caller wraps + * in their own transaction. */ async function stripGroupExecutions( - trx: Parameters[0]>[0], + trx: DbOrTx, tableId: string, groupIds: Iterable ): Promise { - for (const gid of groupIds) { - await trx.execute( - sql`UPDATE user_table_rows SET executions = executions - ${gid}::text WHERE table_id = ${tableId} AND executions ? ${gid}::text` + const ids = Array.from(new Set(groupIds)) + if (ids.length === 0) return + await trx + .delete(tableRowExecutions) + .where( + and(eq(tableRowExecutions.tableId, tableId), inArray(tableRowExecutions.groupId, ids)) as SQL ) - } } /** @@ -1649,13 +1940,16 @@ export async function updateRow( ...(existingRow.data as RowData), ...data.data, } - // Auto-clear exec records for workflow output columns the user just wiped, - // so the auto-fire reactor sees no exec and re-arms the cell. - const effectiveExecutionsPatch = deriveExecClearsForDataPatch( - data.data, - table.schema, - data.executionsPatch - ) + // Auto-clear exec records for workflow output columns the user just wiped + // AND for downstream groups whose deps just changed. Surfaces the in-flight + // downstream groups so the caller can cancel + re-run them. + const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = + deriveExecClearsForDataPatch( + data.data, + table.schema, + existingRow.executions, + data.executionsPatch + ) const mergedExecutions = applyExecutionsPatch(existingRow.executions, effectiveExecutionsPatch) // Validate size @@ -1686,52 +1980,40 @@ export async function updateRow( const now = new Date() - // Cell-task partial writes pass `cancellationGuard` so the SQL update is a - // no-op when (a) a stop click already wrote `cancelled` for this run, or - // (b) a newer run has taken over the cell with a different executionId. The - // worker is "this run's writes only land if this run is still the active - // run on the cell." Authoritative cancel writes from `cancelWorkflowGroupRuns` - // skip the guard entirely (they don't pass `cancellationGuard`). - // - // SQL-level for atomicity: an in-process read + update would race a - // concurrent stop or rerun. The two clauses are joined by AND because - // either failing means the worker is no longer authoritative. + // Cell-task partial writes pass `cancellationGuard` so the upsert into + // `tableRowExecutions` is a no-op when (a) a stop click already wrote + // `cancelled` for this run, or (b) a newer run has taken over the cell + // with a different executionId. Authoritative cancel writes from + // `cancelWorkflowGroupRuns` skip the guard entirely. Data + executions + // commit in one transaction so a partial write can't leave the sidecar + // and the row out of sync. const guard = data.cancellationGuard - const whereClause = guard - ? and( - eq(userTableRows.id, data.rowId), - // Reject writes that would land on top of an already-`cancelled` state - // for this same run. Wrapped in IS DISTINCT FROM so a missing exec - // (NULL) cleanly evaluates as "different" rather than NULL-poisoning. - sql`(executions->${guard.groupId}->>'status' IS DISTINCT FROM 'cancelled' OR executions->${guard.groupId}->>'executionId' IS DISTINCT FROM ${guard.executionId})`, - // Reject writes from a stale worker — the cell's active run has moved - // on. `OR exec IS NULL` lets the worker land its first `running` - // stamp on a row that has no prior exec record (initial stamp from - // the scheduler may not have committed yet). - sql`(executions->${guard.groupId} IS NULL OR executions->${guard.groupId}->>'executionId' = ${guard.executionId})` + const guardRejected = await db + .transaction(async (trx) => { + await trx + .update(userTableRows) + .set({ data: mergedData, updatedAt: now }) + .where(eq(userTableRows.id, data.rowId)) + + const result = await writeExecutionsPatch( + trx, + data.tableId, + data.rowId, + effectiveExecutionsPatch, + guard ) - : eq(userTableRows.id, data.rowId) - - // Apply the executions patch at the SQL level — we never overwrite the full - // executions blob, only the keys the caller explicitly patched. Without - // this, concurrent updateRow calls (e.g., a column edit and a manual - // retry's stamp) would each compute `mergedExecutions` from their own - // in-memory snapshot and the last writer wins, clobbering the other's - // exec keys. The data field still does last-writer-wins because that's - // the user's edit, but exec records are independently keyed by groupId. - const executionsExpr = buildExecutionsSqlPatch(effectiveExecutionsPatch) - const updated = await db - .update(userTableRows) - .set({ - data: mergedData, - ...(executionsExpr ? { executions: executionsExpr } : {}), - updatedAt: now, + if (result === 'guard-rejected') { + // Roll back the data update too — the worker isn't authoritative. + throw new GuardRejected() + } + return false + }) + .catch((err) => { + if (err instanceof GuardRejected) return true + throw err }) - .where(whereClause) - .returning({ id: userTableRows.id }) - // Only meaningful when a guard is set — `null` signals "guard rejected". - if (guard && updated.length === 0) { + if (guardRejected) { return null } @@ -1756,9 +2038,54 @@ export async function updateRow( table.schema, requestId ) - // Awaited (not `void`) so cell tasks dispatch their cascade before the - // trigger.dev worker tears down on `run()` resolve. - if (!data.skipScheduler) await scheduleRunsForRows(table, [updatedRow]) + + // Auto-fire only on user-facing data edits. Internal callers that mutate + // executions (cell-task partial/terminal writes, cancel writes) always pass + // `executionsPatch` — re-dispatching from those would recursively spawn new + // dispatches for every running/terminal write, flooding the dispatcher with + // redundant pre-stamps that strand `pending` cells. + const isInternalExecWrite = data.executionsPatch && Object.keys(data.executionsPatch).length > 0 + if (isInternalExecWrite) { + return updatedRow + } + + // Two passes: + // 1. Cancel in-flight downstream groups whose dep just changed, then + // manually re-run them — the cancel writes `cancelled` per cell and + // `mode: 'incomplete' + isManualRun: true` wipes those entries and + // re-enqueues. + // 2. `mode: 'new'` for groups that just had their exec entries cleared + // (own-output wipe OR terminal downstream dep-changed) — the + // dispatcher's `jsonb_exists_all` SQL filter lets the row through + // because at least one targeted group's exec is now missing. + if (inFlightDownstreamGroups.length > 0) { + void (async () => { + try { + await cancelWorkflowGroupRuns(data.tableId, data.rowId, { + groupIds: inFlightDownstreamGroups, + }) + await runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + mode: 'incomplete', + isManualRun: true, + rowIds: [data.rowId], + groupIds: inFlightDownstreamGroups, + requestId, + }) + } catch (err) { + logger.error(`[${requestId}] cancel+rerun for in-flight downstream groups failed:`, err) + } + })() + } + void runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + rowIds: [data.rowId], + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRow) failed:`, err)) return updatedRow } @@ -1906,7 +2233,7 @@ export async function updateRowsByFilter( const updatedRows: TableRow[] = matchingRows.map((r) => ({ id: r.id, data: { ...(r.data as RowData), ...data.data }, - executions: ((r as { executions?: unknown }).executions as RowExecutions) ?? {}, + executions: {}, position: 0, createdAt: now, updatedAt: now, @@ -1920,7 +2247,14 @@ export async function updateRowsByFilter( table.schema, requestId ) - void scheduleRunsForRows(table, updatedRows) + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: updatedRows.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (updateRowsByFilter) failed:`, err)) return { affectedCount: matchingRows.length, @@ -1946,7 +2280,6 @@ export async function batchUpdateRows( .select({ id: userTableRows.id, data: userTableRows.data, - executions: userTableRows.executions, }) .from(userTableRows) .where( @@ -1957,11 +2290,16 @@ export async function batchUpdateRows( ) ) + const executionsByRow = await loadExecutionsByRow( + db, + existingRows.map((r) => r.id) + ) + type ExistingRow = { data: RowData; executions: RowExecutions } const existingMap = new Map( existingRows.map((r) => [ r.id, - { data: r.data as RowData, executions: (r.executions as RowExecutions) ?? {} }, + { data: r.data as RowData, executions: executionsByRow.get(r.id) ?? {} }, ]) ) @@ -1975,17 +2313,22 @@ export async function batchUpdateRows( mergedData: RowData mergedExecutions: RowExecutions executionsPatch?: Record + inFlightDownstreamGroups: string[] }> = [] for (const update of data.updates) { const existing = existingMap.get(update.rowId)! const merged = { ...existing.data, ...update.data } // Auto-clear exec records for workflow output columns the user just - // wiped — same rationale as `updateRow`. - const effectiveExecutionsPatch = deriveExecClearsForDataPatch( - update.data, - table.schema, - update.executionsPatch - ) + // wiped AND downstream dep-changed terminal groups — same rationale as + // `updateRow`. Per-row in-flight downstream groups are surfaced so we + // can run the cancel+rerun orchestration after the batch commits. + const { executionsPatch: effectiveExecutionsPatch, inFlightDownstreamGroups } = + deriveExecClearsForDataPatch( + update.data, + table.schema, + existing.executions, + update.executionsPatch + ) const mergedExecutions = applyExecutionsPatch(existing.executions, effectiveExecutionsPatch) const sizeValidation = validateRowSize(merged) @@ -2003,6 +2346,7 @@ export async function batchUpdateRows( mergedData: merged, mergedExecutions, executionsPatch: effectiveExecutionsPatch, + inFlightDownstreamGroups, }) } @@ -2027,21 +2371,18 @@ export async function batchUpdateRows( await setTableTxTimeouts(trx, { statementMs: 60_000 }) for (let i = 0; i < mergedUpdates.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { const batch = mergedUpdates.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) - // Same as `updateRow`: patch executions at the SQL level when a patch - // is set, so concurrent writers don't clobber each other's keys via - // last-writer-wins on the full jsonb blob. - const updatePromises = batch.map(({ rowId, mergedData, executionsPatch }) => { - const executionsExpr = buildExecutionsSqlPatch(executionsPatch) - return trx + // Update row data in parallel; sidecar exec writes are sequential per + // row (each goes through writeExecutionsPatch's per-key upsert). + const dataPromises = batch.map(({ rowId, mergedData }) => + trx .update(userTableRows) - .set({ - data: mergedData, - ...(executionsExpr ? { executions: executionsExpr } : {}), - updatedAt: now, - }) + .set({ data: mergedData, updatedAt: now }) .where(eq(userTableRows.id, rowId)) - }) - await Promise.all(updatePromises) + ) + await Promise.all(dataPromises) + for (const { rowId, executionsPatch } of batch) { + await writeExecutionsPatch(trx, data.tableId, rowId, executionsPatch) + } } }) @@ -2069,7 +2410,47 @@ export async function batchUpdateRows( table.schema, requestId ) - if (!data.skipScheduler) void scheduleRunsForRows(table, updatedRowsForTrigger) + // Per-row cancel+rerun for in-flight downstream groups whose deps just + // changed — same orchestration as single-row `updateRow`. Without this, + // batch updates would leave running workflows reading stale dep values. + // Each row needs its own cancel + manual-incomplete dispatch because + // `cancelWorkflowGroupRuns`'s `groupIds` filter is per-row. + const rowsWithInFlightDownstream = mergedUpdates.filter( + (u) => u.inFlightDownstreamGroups.length > 0 + ) + if (rowsWithInFlightDownstream.length > 0) { + void (async () => { + try { + for (const { rowId, inFlightDownstreamGroups } of rowsWithInFlightDownstream) { + await cancelWorkflowGroupRuns(data.tableId, rowId, { + groupIds: inFlightDownstreamGroups, + }) + await runWorkflowColumn({ + tableId: data.tableId, + workspaceId: data.workspaceId, + mode: 'incomplete', + isManualRun: true, + rowIds: [rowId], + groupIds: inFlightDownstreamGroups, + requestId, + }) + } + } catch (err) { + logger.error( + `[${requestId}] cancel+rerun for in-flight downstream groups (batch) failed:`, + err + ) + } + })() + } + void runWorkflowColumn({ + tableId: table.id, + workspaceId: table.workspaceId, + rowIds: updatedRowsForTrigger.map((r) => r.id), + mode: 'new', + isManualRun: false, + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (batchUpdateRows) failed:`, err)) return { affectedCount: mergedUpdates.length, @@ -2789,18 +3170,20 @@ export async function addWorkflowGroup( updatedAt: now, } - // Schedule existing rows so already-filled deps trigger immediately. Skipped - // when the caller opted out (Mothership stages groups silently — `autoRun: - // false` — so the AI can compose multiple changes without firing rows mid-edit). - // Awaited (not `void`) so the response includes the queued exec state — the - // client's post-mutation refetch otherwise lands before the stamps commit - // and the rows query polling never starts. + // Auto-fire existing rows whose deps are already met for the new group. + // Fire-and-forget — the dispatcher bounds queue depth (window of 20) and + // walks the table in the background. HTTP returns instantly; cells fill + // in over the next minutes as the dispatcher walks. Mothership opts out + // by setting `autoRun: false`. if (data.autoRun !== false) { - try { - await scheduleRunsForTable(updatedTable) - } catch (err) { - logger.error(`[${requestId}] Failed to schedule runs after group add:`, err) - } + void runWorkflowColumn({ + tableId: updatedTable.id, + workspaceId: updatedTable.workspaceId, + mode: 'new', + isManualRun: false, + groupIds: [data.group.id], + requestId, + }).catch((err) => logger.error(`[${requestId}] auto-dispatch (addWorkflowGroup) failed:`, err)) } return updatedTable @@ -3086,16 +3469,20 @@ export async function updateWorkflowGroup( } } - // autoRun toggled false → true: fire deps-satisfied rows now. Mirrors the - // post-add scheduling path so re-enabling auto-fire doesn't require manual - // run clicks for rows that are already eligible. Awaited so the post- - // mutation refetch sees the queued exec stamps. + // autoRun toggled false → true: fire deps-satisfied rows now via the + // dispatcher. Mirrors the post-add path so re-enabling auto-fire doesn't + // require manual run clicks for rows that are already eligible. if (group.autoRun === false && data.autoRun === true) { - try { - await scheduleRunsForTable(updatedTable, { groupId: data.groupId }) - } catch (err) { - logger.error(`[${requestId}] Failed to schedule runs after autoRun toggled on:`, err) - } + void runWorkflowColumn({ + tableId: updatedTable.id, + workspaceId: updatedTable.workspaceId, + mode: 'new', + isManualRun: false, + groupIds: [data.groupId], + requestId, + }).catch((err) => + logger.error(`[${requestId}] auto-dispatch (updateWorkflowGroup autoRun=true) failed:`, err) + ) } return updatedTable @@ -3492,20 +3879,40 @@ async function backfillGroupOutputsFromLogs(opts: { const { pluckByPath } = await import('./pluck') - const rowRecords = await db - .select() - .from(userTableRows) - .where(eq(userTableRows.tableId, table.id)) + // Find rows whose group execution completed and grab their executionId + // directly from the sidecar — hits the (table_id, group_id) index, no + // table scan over rowdata. + const completedExecs = await db + .select({ + rowId: tableRowExecutions.rowId, + executionId: tableRowExecutions.executionId, + }) + .from(tableRowExecutions) + .where( + and( + eq(tableRowExecutions.tableId, table.id), + eq(tableRowExecutions.groupId, groupId), + eq(tableRowExecutions.status, 'completed') + ) + ) - // Collect unique executionIds across rows whose group execution completed. const executionIdsByRow = new Map() - for (const r of rowRecords) { - const exec = (r.executions as RowExecutions)?.[groupId] - if (!exec || exec.status !== 'completed' || !exec.executionId) continue - executionIdsByRow.set(r.id, exec.executionId) + for (const e of completedExecs) { + if (!e.executionId) continue + executionIdsByRow.set(e.rowId, e.executionId) } if (executionIdsByRow.size === 0) return + const rowRecords = await db + .select({ id: userTableRows.id, data: userTableRows.data }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, table.id), + inArray(userTableRows.id, Array.from(executionIdsByRow.keys())) + ) + ) + const executionIds = Array.from(new Set(executionIdsByRow.values())) const logs = await db .select({ @@ -3525,9 +3932,9 @@ async function backfillGroupOutputsFromLogs(opts: { const updates: Array<{ rowId: string; data: RowData }> = [] for (const r of rowRecords) { - const exec = (r.executions as RowExecutions)?.[groupId] - if (!exec?.executionId) continue - const log = logByExecutionId.get(exec.executionId) + const execId = executionIdsByRow.get(r.id) + if (!execId) continue + const log = logByExecutionId.get(execId) if (!log) continue const dataPatch: RowData = {} diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index 52e81b73f77..09fd6fcfb9c 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -70,9 +70,9 @@ export interface WorkflowGroup { } /** - * Per-row execution state for one workflow group, stored in - * `userTableRows.executions[groupId]`. Holds run metadata only — picked - * values land in `row.data` directly. + * Per-row execution state for one workflow group, persisted as a row in the + * `tableRowExecutions` sidecar keyed by `(rowId, groupId)`. Holds run + * metadata only — picked output values land in `row.data` directly. */ export interface RowExecutionMetadata { status: 'pending' | 'queued' | 'running' | 'completed' | 'error' | 'cancelled' @@ -94,6 +94,10 @@ export interface RowExecutionMetadata { * block should render `Error`, not every output column. */ blockErrors?: Record + /** ISO timestamp set when a cell is cancelled. The dispatcher skips + * re-runs whose `cancelledAt > dispatch.requestedAt` — a user cancel + * mid-dispatch must not be overridden by `isManualRun`. */ + cancelledAt?: string } /** Map of `WorkflowGroup.id` → execution state. Stored on every row. */ @@ -295,9 +299,10 @@ export interface UpdateRowData { data: RowData workspaceId: string /** - * Optional partial patch to merge into `userTableRows.executions`. Top-level - * keys are `WorkflowGroup.id`; pass `null` for a key to delete that group's - * execution state. Used by the cell task and cancel paths. + * Optional partial patch to apply to the row's `tableRowExecutions` + * entries. Top-level keys are `WorkflowGroup.id`; pass `null` for a key + * to delete that group's execution row. Used by the cell task and cancel + * paths. */ executionsPatch?: Record /** @@ -308,14 +313,6 @@ export interface UpdateRowData { * state. `updateRow` returns `null` when the guard rejects the write. */ cancellationGuard?: { groupId: string; executionId: string } - /** - * When true, the post-write `scheduleRunsForRows` call is skipped. Used by - * the cancel path (which is tearing rows down, not waking them up) and by - * the manual-run path (which fires its own `scheduleRunsForRows` with - * `isManualRun: true` and doesn't want a duplicate auto-fire pass on the - * cleared cells). Default false: every other write fires the reactor. - */ - skipScheduler?: boolean } export interface BulkUpdateData { @@ -332,8 +329,6 @@ export interface BatchUpdateByIdData { executionsPatch?: Record }> workspaceId: string - /** Same semantics as `UpdateRowData.skipScheduler`. */ - skipScheduler?: boolean } export interface BulkDeleteData { diff --git a/apps/sim/lib/table/workflow-columns.ts b/apps/sim/lib/table/workflow-columns.ts index da912e9d32e..686f97dbc6e 100644 --- a/apps/sim/lib/table/workflow-columns.ts +++ b/apps/sim/lib/table/workflow-columns.ts @@ -6,14 +6,14 @@ */ import { db } from '@sim/db' -import { pausedExecutions, userTableRows } from '@sim/db/schema' +import { pausedExecutions, tableRowExecutions, type userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, asc, eq, inArray, sql } from 'drizzle-orm' -import { getJobQueue } from '@/lib/core/async-jobs/config' +import { and, eq, inArray, sql } from 'drizzle-orm' import type { EnqueueOptions } from '@/lib/core/async-jobs/types' -import { buildCancelledExecution, writeWorkflowGroupState } from '@/lib/table/cell-write' +import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { buildCancelledExecution } from '@/lib/table/cell-write' import type { RowData, RowExecutionMetadata, @@ -27,6 +27,7 @@ import type { const logger = createLogger('WorkflowGroupScheduler') import { areGroupDepsSatisfied, areOutputsFilled, isExecInFlight } from './deps' +import type { DispatchMode } from './dispatcher' export { getUnmetGroupDeps, @@ -54,14 +55,16 @@ export type EligibilityReason = | 'in-flight' | 'completed-on-auto' | 'error-on-auto' + | 'cancelled-on-auto' | 'completed-on-incomplete' + | 'has-prior-attempt' | 'manual-bypass' | 'deps-unmet' export function classifyEligibility( group: WorkflowGroup, row: TableRow, - opts?: { isManualRun?: boolean; mode?: 'all' | 'incomplete' } + opts?: { isManualRun?: boolean; mode?: DispatchMode } ): EligibilityReason { const isManualRun = opts?.isManualRun ?? false const mode = opts?.mode ?? 'all' @@ -69,14 +72,27 @@ export function classifyEligibility( if (group.autoRun === false && !isManualRun) return 'autoRun-off' const exec = row.executions?.[group.id] - if (isExecInFlight(exec)) return 'in-flight' + // Dispatcher pre-stamp orphans (`pending` + `executionId: null`) are + // placeholders left behind when a previous dispatcher loop wrote the stamp + // but no cell-task picked up (cascade-lock contention, trigger.dev queue + // failure, etc.). Treat them as claimable so a new dispatcher can re-enqueue + // — without this carve-out the row would render "Queued" forever. Matches + // the `pickNextEligibleGroupForRow` cascade-loop carve-out. + const isOrphanPreStamp = exec?.status === 'pending' && exec.executionId == null + if (!isOrphanPreStamp && isExecInFlight(exec)) return 'in-flight' const status = exec?.status + // `mode: 'new'` is the auto-fire scope: only rows that have never been + // attempted on this group run. Any pre-existing exec entry — completed, + // cancelled, or error — keeps the cell sticky until the user manually + // re-runs via "Run column" / "Run all rows" / "Run this row". + // Exception: orphan pre-stamps are claimable (handled above). + if (mode === 'new' && exec && !isOrphanPreStamp) return 'has-prior-attempt' + const completedAndFilled = status === 'completed' && areOutputsFilled(group, row) if (!isManualRun && completedAndFilled) return 'completed-on-auto' - // Auto-fire skips `error` to avoid infinite-retry loops on a deterministic - // failure. `cancelled` is left runnable — cancellation is user-initiated. if (!isManualRun && status === 'error') return 'error-on-auto' + if (!isManualRun && status === 'cancelled') return 'cancelled-on-auto' if (mode === 'incomplete' && completedAndFilled) return 'completed-on-incomplete' if (isManualRun && group.autoRun === false) return 'manual-bypass' @@ -92,6 +108,35 @@ export function isGroupEligible( return reason === 'eligible' || reason === 'manual-bypass' } +/** Walks a row's workflow groups (in `workflowGroups` order) and returns the + * first one whose deps are met and that isn't already in-flight under a + * different worker. Skips `excludeGroupId` (the group we just finished in + * the cascade loop, to prevent self-retrigger). The cascade-loop is allowed + * to claim past a dispatcher pre-stamp (`pending` with `executionId: null`) + * — that's a placeholder, not a real worker claim. */ +export function pickNextEligibleGroupForRow( + table: TableDefinition, + row: TableRow, + excludeGroupId?: string +): WorkflowGroup | null { + const groups = table.schema.workflowGroups ?? [] + for (const group of groups) { + if (group.id === excludeGroupId) continue + const exec = row.executions?.[group.id] + // Dispatcher pre-stamp (pending + executionId: null) is a placeholder; the + // cascade-loop is the right owner of the claim. Treat as "claimable" by + // pretending the exec doesn't exist for the eligibility check. + const effectiveRow = + exec?.status === 'pending' && exec.executionId == null + ? { ...row, executions: { ...row.executions, [group.id]: undefined } as RowExecutions } + : row + if (isGroupEligible(group, effectiveRow, { isManualRun: false, mode: 'incomplete' })) { + return group + } + } + return null +} + /** * Shared options for the three `scheduleRuns*` entry points. `isManualRun` * flips two gates in the eligibility predicate so a manual click can re-run @@ -101,177 +146,153 @@ export interface ScheduleOpts { groupId?: string groupIds?: string[] isManualRun?: boolean - mode?: 'all' | 'incomplete' + mode?: DispatchMode } -/** - * Re-evaluate eligibility on these specific rows and enqueue runnable cells. - * The hot path: every row write (insert / update / cascade) calls this with the - * just-written row(s). - */ -export async function scheduleRunsForRows( +/** Pure eligibility filter + payload building. Shared by the auto-fire path + * (`scheduleRunsForRows`) and the dispatcher's per-window batch path. */ +export function buildPendingRuns( table: TableDefinition, rows: TableRow[], opts?: ScheduleOpts -): Promise<{ triggered: number }> { - try { - const allGroups = table.schema.workflowGroups ?? [] - if (allGroups.length === 0) return { triggered: 0 } - if (rows.length === 0) return { triggered: 0 } - - const groupIdFilter = opts?.groupIds - ? new Set(opts.groupIds) - : opts?.groupId - ? new Set([opts.groupId]) - : null - const groups = groupIdFilter ? allGroups.filter((g) => groupIdFilter.has(g.id)) : allGroups - if (groups.length === 0) return { triggered: 0 } - - const orderedRows = rows.length <= 1 ? rows : [...rows].sort((a, b) => a.position - b.position) - - const pendingRuns: RunGroupCellOptions[] = [] - const reasonCounts: Partial> = {} - - for (const row of orderedRows) { - for (const group of groups) { - const reason = classifyEligibility(group, row, { - isManualRun: opts?.isManualRun, - mode: opts?.mode, - }) - reasonCounts[reason] = (reasonCounts[reason] ?? 0) + 1 - if (reason !== 'eligible' && reason !== 'manual-bypass') continue - pendingRuns.push({ - tableId: table.id, - tableName: table.name, - rowId: row.id, - groupId: group.id, - workflowId: group.workflowId, - workspaceId: table.workspaceId, - executionId: generateId(), - }) - } +): WorkflowGroupCellPayload[] { + const allGroups = table.schema.workflowGroups ?? [] + if (allGroups.length === 0) return [] + if (rows.length === 0) return [] + + const groupIdFilter = opts?.groupIds + ? new Set(opts.groupIds) + : opts?.groupId + ? new Set([opts.groupId]) + : null + const groups = groupIdFilter ? allGroups.filter((g) => groupIdFilter.has(g.id)) : allGroups + if (groups.length === 0) return [] + + const orderedRows = rows.length <= 1 ? rows : [...rows].sort((a, b) => a.position - b.position) + + const pendingRuns: WorkflowGroupCellPayload[] = [] + const reasonCounts: Partial> = {} + + for (const row of orderedRows) { + for (const group of groups) { + const reason = classifyEligibility(group, row, { + isManualRun: opts?.isManualRun, + mode: opts?.mode, + }) + reasonCounts[reason] = (reasonCounts[reason] ?? 0) + 1 + if (reason !== 'eligible' && reason !== 'manual-bypass') continue + pendingRuns.push({ + tableId: table.id, + tableName: table.name, + rowId: row.id, + groupId: group.id, + workflowId: group.workflowId, + workspaceId: table.workspaceId, + executionId: generateId(), + }) } + } - logger.debug( - `[Cascade] table=${table.id} rows=${rows.length} groups=${groups.length} manual=${opts?.isManualRun ?? false} mode=${opts?.mode ?? 'all'} reasons=${JSON.stringify(reasonCounts)}` - ) - - if (pendingRuns.length === 0) return { triggered: 0 } + logger.debug( + `[Cascade] table=${table.id} rows=${rows.length} groups=${groups.length} manual=${opts?.isManualRun ?? false} mode=${opts?.mode ?? 'all'} reasons=${JSON.stringify(reasonCounts)}` + ) - logger.info(`Scheduling ${pendingRuns.length} workflow group cell run(s) for table=${table.id}`) + return pendingRuns +} - const queue = await getJobQueue() - const { executeWorkflowGroupCellJob } = await import('@/background/workflow-column-execution') - const items = pendingRuns.map((runOpts) => ({ - payload: runOpts, - options: { - metadata: { +/** Build the per-cell `{payload, options}` items for `queue.batchEnqueue` / + * `queue.batchEnqueueAndWait`. Hydrates trigger.dev tags, concurrency keys, + * the inline runner, and the cancel key the inline backend uses to map a + * Stop click to the in-flight cell's AbortController. */ +export async function buildEnqueueItems( + pendingRuns: WorkflowGroupCellPayload[] +): Promise> { + const { executeWorkflowGroupCellJob } = await import('@/background/workflow-column-execution') + return pendingRuns.map((runOpts) => ({ + payload: runOpts, + options: { + metadata: { + workflowId: runOpts.workflowId, + workspaceId: runOpts.workspaceId, + correlation: { + executionId: runOpts.executionId, + requestId: `wfgrp-${runOpts.executionId}`, + source: 'workflow' as const, workflowId: runOpts.workflowId, - workspaceId: runOpts.workspaceId, - correlation: { - executionId: runOpts.executionId, - requestId: `wfgrp-${runOpts.executionId}`, - source: 'workflow' as const, - workflowId: runOpts.workflowId, - triggerType: 'table', - }, + triggerType: 'table', }, - concurrencyKey: runOpts.tableId, - concurrencyLimit: TABLE_CONCURRENCY_LIMIT, - tags: [`tableId:${runOpts.tableId}`, `rowId:${runOpts.rowId}`, `group:${runOpts.groupId}`], - runner: executeWorkflowGroupCellJob as EnqueueOptions['runner'], }, - })) - - let jobIds: string[] - try { - jobIds = await queue.batchEnqueue('workflow-group-cell', items) - } catch (err) { - logger.error(`Batch enqueue failed for table=${table.id}:`, err) - await Promise.allSettled( - pendingRuns.map((runOpts) => - writeWorkflowGroupState(runOpts, { - executionState: { - status: 'error', - executionId: runOpts.executionId, - jobId: null, - workflowId: runOpts.workflowId, - error: toError(err).message, - }, - }) - ) - ) - return { triggered: 0 } - } - - // Stamp `queued` in chunks of `TABLE_CONCURRENCY_LIMIT`. Within a chunk we - // parallelize the writes (no ordering constraint); across chunks we await - // serially so trigger.dev still picks rows up in submission order — the - // concurrency cap means at most one chunk is in flight per table anyway. - for (let i = 0; i < pendingRuns.length; i += TABLE_CONCURRENCY_LIMIT) { - const chunk = pendingRuns.slice(i, i + TABLE_CONCURRENCY_LIMIT) - const ids = jobIds.slice(i, i + TABLE_CONCURRENCY_LIMIT) - await Promise.all(chunk.map((run, j) => stampQueuedOrCancel(queue, run, ids[j]))) - } - return { triggered: pendingRuns.length } - } catch (err) { - logger.error('scheduleRunsForRows failed:', err) - return { triggered: 0 } - } + concurrencyKey: runOpts.tableId, + concurrencyLimit: TABLE_CONCURRENCY_LIMIT, + tags: cellTagsFor(runOpts), + runner: executeWorkflowGroupCellJob as EnqueueOptions['runner'], + cancelKey: cellCancelKey(runOpts.tableId, runOpts.rowId, runOpts.groupId), + }, + })) } -/** - * Re-evaluate eligibility on every row of the table. Used after schema changes - * (workflow group added, autoRun toggled on) where we don't have a list of - * just-written rows but need to fire any newly-eligible (row × group) pair. - */ -export async function scheduleRunsForTable( - table: TableDefinition, - opts?: ScheduleOpts -): Promise<{ triggered: number }> { - const rows = await fetchAllRows(table.id) - return scheduleRunsForRows(table, rows, opts) -} - -/** - * Re-evaluate eligibility on the rows with these ids. Sugar for callers that - * have row ids but not materialized rows. - */ -async function scheduleRunsForRowIds( - table: TableDefinition, - rowIds: string[], - opts?: ScheduleOpts -): Promise<{ triggered: number }> { - if (rowIds.length === 0) return { triggered: 0 } - const rows = await fetchRowsByIds(table.id, rowIds) - return scheduleRunsForRows(table, rows, opts) +/** Stable key for `cancelInlineRun` lookups. Stamped on every enqueue item by + * `buildEnqueueItems`; the cancel path computes the same key per cell. */ +export function cellCancelKey(tableId: string, rowId: string, groupId: string): string { + return `${tableId}:${rowId}:${groupId}` } -async function fetchAllRows(tableId: string): Promise { - const records = await db.select().from(userTableRows).where(eq(userTableRows.tableId, tableId)) - return records.map(toTableRow) +/** Trigger.dev tags stamped on every `workflow-group-cell` run so tag-based + * cancel (`runs.list({ tag })` + `runs.cancel(id)`) can target a specific + * cell or table without needing per-cell jobIds. */ +export function cellTagsFor(runOpts: WorkflowGroupCellPayload): string[] { + return [`tableId:${runOpts.tableId}`, `rowId:${runOpts.rowId}`, `group:${runOpts.groupId}`] } -async function fetchRowsByIds(tableId: string, rowIds: string[]): Promise { - const records = await db - .select() - .from(userTableRows) - .where(and(eq(userTableRows.tableId, tableId), inArray(userTableRows.id, rowIds))) - return records.map(toTableRow) +/** Cancel every active trigger.dev `workflow-group-cell` run whose tags + * match. Paginates `runs.list` and fires `runs.cancel` per match. Errors + * are logged and swallowed — the cell-write SQL guard already makes + * workers no-op on cancelled rows whether or not trigger.dev acked the + * cancel, so partial failure is safe. */ +export async function cancelCellRunsByTags(tags: string[]): Promise { + if (tags.length === 0) return + const { runs } = await import('@trigger.dev/sdk') + const cancellations: Array> = [] + try { + // Trigger.dev paginates with auto-iterating cursor — looping the page + // iterator is the documented usage pattern. + for await (const run of runs.list({ + tag: tags, + taskIdentifier: 'workflow-group-cell', + status: ['PENDING_VERSION', 'QUEUED', 'DEQUEUED', 'EXECUTING', 'WAITING', 'DELAYED'], + })) { + cancellations.push( + runs.cancel(run.id).catch((err) => { + logger.warn(`cancelCellRunsByTags: cancel ${run.id} failed`, { + error: toError(err).message, + }) + }) + ) + } + await Promise.allSettled(cancellations) + } catch (err) { + logger.warn(`cancelCellRunsByTags: list failed`, { + tags, + error: toError(err).message, + }) + } } -function toTableRow(r: typeof userTableRows.$inferSelect): TableRow { +export function toTableRow( + r: typeof userTableRows.$inferSelect, + executions: RowExecutions = {} +): TableRow { return { id: r.id, data: r.data as RowData, - executions: (r.executions as RowExecutions) ?? {}, + executions, position: r.position, createdAt: r.createdAt, updatedAt: r.updatedAt, } } -interface RunGroupCellOptions { +export interface WorkflowGroupCellPayload { tableId: string tableName: string rowId: string @@ -282,50 +303,25 @@ interface RunGroupCellOptions { } /** Per-table concurrency cap. Mirrors trigger.dev's `concurrencyLimit: 20`. */ -const TABLE_CONCURRENCY_LIMIT = 20 - -async function stampQueuedOrCancel( - queue: Awaited>, - opts: RunGroupCellOptions, - jobId: string -): Promise { - let stampResult: 'wrote' | 'skipped' = 'wrote' - try { - stampResult = await writeWorkflowGroupState(opts, { - executionState: { - status: 'queued', - executionId: opts.executionId, - jobId, - workflowId: opts.workflowId, - error: null, - }, - }) - } catch (err) { - logger.error( - `Failed to stamp queued state (table=${opts.tableId} row=${opts.rowId} group=${opts.groupId}):`, - err - ) - } - - if (stampResult === 'skipped') { - try { - await queue.cancelJob(jobId) - } catch (cancelErr) { - logger.error(`Failed to cancel orphaned workflow-group-cell job (jobId=${jobId}):`, cancelErr) - } - } -} +export const TABLE_CONCURRENCY_LIMIT = 20 /** * Cancels in-flight workflow-group runs for a table or single row. Writes * `cancelled` authoritatively for every `running` or `pending` group * execution — the client-side write is the source of truth, independent of * whether the trigger.dev cancel reaches the worker before its terminal - * write. + * write. Pass `groupIds` to restrict the cancel to a subset of groups on + * the row (used by `updateRow` to cancel only the downstream groups whose + * deps just changed). */ -export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): Promise { +export async function cancelWorkflowGroupRuns( + tableId: string, + rowId?: string, + options?: { groupIds?: string[] } +): Promise { const { getTableById, updateRow } = await import('@/lib/table/service') const { getJobQueue } = await import('@/lib/core/async-jobs/config') + const { listActiveDispatches, markActiveDispatchesCancelled } = await import('./dispatcher') const table = await getTableById(tableId) if (!table) { @@ -333,22 +329,66 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): return 0 } - const groups = table.schema.workflowGroups ?? [] - if (groups.length === 0) return 0 - const groupIds = new Set(groups.map((g) => g.id)) + // Per-row cancel leaves the dispatcher alone — other rows in the same + // dispatch keep running. Table-wide cancel must stop it, else the cursor + // marches on and re-enqueues fresh cells past what we just cancelled. + if (!rowId) { + await markActiveDispatchesCancelled(tableId) + } + + const allGroups = table.schema.workflowGroups ?? [] + if (allGroups.length === 0) return 0 + const groupIds = options?.groupIds + ? new Set(allGroups.filter((g) => options.groupIds?.includes(g.id)).map((g) => g.id)) + : new Set(allGroups.map((g) => g.id)) + if (groupIds.size === 0) return 0 + + // Per-row Stop on a row the dispatcher hasn't reached yet has no sidecar + // entry to cancel — the dispatcher would later walk to that row, see no + // exec, classify eligible, and re-fire. Pre-write `cancelled` tombstones + // for active-dispatch in-scope groups so the existing `cancelledAt > + // dispatch.requestedAt` filter in `dispatcherStep` catches them. Skip + // when there's no active dispatch (nothing to outrun). + let aheadOfCursorTombstones: Array<{ groupId: string; workflowId: string }> = [] + if (rowId) { + const activeDispatches = await listActiveDispatches(tableId) + const relevant = activeDispatches.filter((d) => { + if (d.scope.rowIds && !d.scope.rowIds.includes(rowId)) return false + return d.scope.groupIds.some((gid) => groupIds.has(gid)) + }) + if (relevant.length > 0) { + // Intersection of targeted groups with active-dispatch scopes — only + // these groups are at risk of being re-fired by an in-progress dispatch. + const atRisk = new Set() + for (const d of relevant) { + for (const gid of d.scope.groupIds) { + if (groupIds.has(gid)) atRisk.add(gid) + } + } + aheadOfCursorTombstones = Array.from(atRisk).map((gid) => ({ + groupId: gid, + workflowId: allGroups.find((g) => g.id === gid)?.workflowId ?? '', + })) + } + } // Always filter by tableId — for the per-row case this prevents a // cross-table rowId from doing a wasted DB round-trip and silently // under-counting in the response. For the table-wide case it's the // primary filter. - const rows = await db + const inFlightStatuses = ['running', 'queued', 'pending'] + const inFlightFilters = [ + eq(tableRowExecutions.tableId, tableId), + inArray(tableRowExecutions.status, inFlightStatuses), + inArray(tableRowExecutions.groupId, Array.from(groupIds)), + ] + if (rowId) { + inFlightFilters.push(eq(tableRowExecutions.rowId, rowId)) + } + const inFlightRows = await db .select() - .from(userTableRows) - .where( - rowId - ? and(eq(userTableRows.id, rowId), eq(userTableRows.tableId, tableId)) - : eq(userTableRows.tableId, tableId) - ) + .from(tableRowExecutions) + .where(and(...inFlightFilters)) const queue = await getJobQueue() @@ -358,45 +398,59 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): jobIds: string[] cancelledCount: number } - const mutations: RowMutation[] = [] - - for (const row of rows) { - const executions = (row.executions ?? {}) as RowExecutions - const executionsPatch: Record = {} - const jobIds: string[] = [] - let cancelledCount = 0 - for (const [gid, exec] of Object.entries(executions)) { - if (!groupIds.has(gid)) continue - // `pending` covers the post-reset, pre-dispatch window; `queued` covers - // the post-enqueue, pre-pickup window — a stop click in either state - // must still stick once the worker picks the row up. - if (exec.status !== 'running' && exec.status !== 'queued' && exec.status !== 'pending') - continue - if (exec.jobId) jobIds.push(exec.jobId) - executionsPatch[gid] = buildCancelledExecution(exec) - cancelledCount++ + const byRow = new Map() + + for (const r of inFlightRows) { + const prev: RowExecutionMetadata = { + status: r.status as RowExecutionMetadata['status'], + executionId: r.executionId ?? null, + jobId: r.jobId ?? null, + workflowId: r.workflowId, + error: r.error ?? null, + ...(r.blockErrors && Object.keys(r.blockErrors as Record).length > 0 + ? { blockErrors: r.blockErrors as Record } + : {}), } - if (cancelledCount > 0) { - mutations.push({ rowId: row.id, executionsPatch, jobIds, cancelledCount }) + const existing = byRow.get(r.rowId) ?? { + rowId: r.rowId, + executionsPatch: {}, + jobIds: [], + cancelledCount: 0, } + if (prev.jobId) existing.jobIds.push(prev.jobId) + existing.executionsPatch[r.groupId] = buildCancelledExecution(prev) + existing.cancelledCount++ + byRow.set(r.rowId, existing) } - // Cancel jobs and write rows in parallel — no ordering dependency, so - // serializing dozens-to-hundreds of rows per stop click is pure latency. - await Promise.allSettled( - mutations.flatMap((m) => + const mutations: RowMutation[] = Array.from(byRow.values()) + + // Abort in-flight cell runs. The interface method `cancelByKey` is a no-op + // on the trigger.dev backend (no in-process AbortControllers) and aborts + // the matching AbortController on the database backend. Trigger.dev's tag + // sweep covers the SaaS path; the cell-write SQL guard is the + // authoritative stop signal regardless of backend. + for (const m of mutations) { + for (const gid of Object.keys(m.executionsPatch)) { + queue.cancelByKey(cellCancelKey(tableId, m.rowId, gid)) + } + } + const tagSweepPromise = isTriggerDevEnabled + ? cancelCellRunsByTags(rowId ? [`rowId:${rowId}`] : [`tableId:${tableId}`]) + : Promise.resolve() + await Promise.allSettled([ + ...mutations.flatMap((m) => m.jobIds.map((jobId) => queue.cancelJob(jobId).catch((err) => { logger.error(`Failed to cancel job ${jobId} for ${tableId}/${m.rowId}:`, err) }) ) - ) - ) - // `skipScheduler: true` — we're tearing rows down, not waking them up. The - // auto-fire reactor would otherwise see independent (row, group) pairs whose - // deps are now satisfied (because the upstream group already wrote its - // output before the cancel) and re-enqueue them, which is exactly what the - // user clicked Stop to prevent. + ), + tagSweepPromise, + ]) + // `updateRow` no longer auto-fires the dispatcher post-write — the reactor + // was removed. Cancel-writes only touch executions[gid] state; no risk of + // re-enqueueing what we just cancelled. await Promise.allSettled( mutations.map((m) => updateRow( @@ -406,7 +460,6 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): data: {}, workspaceId: table.workspaceId, executionsPatch: m.executionsPatch, - skipScheduler: true, }, table, `wfgrp-cancel-${m.rowId}` @@ -416,6 +469,45 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): ) ) + // Tombstones for ahead-of-cursor groups. The in-flight cancel writes above + // already cover groups that have a sidecar entry; we only need fresh + // tombstones for groups that don't (the dispatcher hasn't reached them + // yet, so there's nothing to cancel — but without a tombstone the + // dispatcher would still re-fire when its cursor walks to this row). + if (rowId && aheadOfCursorTombstones.length > 0) { + const alreadyHandled = new Set(mutations.flatMap((m) => Object.keys(m.executionsPatch))) + const needsTombstone = aheadOfCursorTombstones.filter((t) => !alreadyHandled.has(t.groupId)) + if (needsTombstone.length > 0) { + const now = new Date() + await Promise.allSettled( + needsTombstone.map((t) => + db + .insert(tableRowExecutions) + .values({ + tableId, + rowId, + groupId: t.groupId, + status: 'cancelled', + executionId: null, + jobId: null, + workflowId: t.workflowId, + error: 'Cancelled', + runningBlockIds: [], + blockErrors: {}, + cancelledAt: now, + updatedAt: now, + }) + .onConflictDoNothing({ + target: [tableRowExecutions.rowId, tableRowExecutions.groupId], + }) + .catch((err) => { + logger.error(`Failed to write tombstone for ${tableId}/${rowId}/${t.groupId}:`, err) + }) + ) + ) + } + } + return mutations.reduce((sum, m) => sum + m.cancelledCount, 0) } @@ -429,96 +521,122 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string): export async function runWorkflowColumn(opts: { tableId: string workspaceId: string - mode: 'all' | 'incomplete' + mode: DispatchMode requestId: string groupIds?: string[] rowIds?: string[] -}): Promise<{ triggered: number }> { + /** When false, eligibility honors `autoRun: false` and treats completed + * cells as terminal — appropriate for auto-fire after row writes or + * schema changes. Defaults to true (user-initiated "Run column"). */ + isManualRun?: boolean +}): Promise<{ dispatchId: string | null }> { const { tableId, workspaceId, mode, requestId, groupIds, rowIds } = opts - const { getTableById, batchUpdateRows } = await import('./service') + const isManualRun = opts.isManualRun ?? true + // Empty `rowIds` array means "scope explicitly empty" — auto-fire callers + // (CSV import on zero matches, etc.) end up here. Skip the dispatch entirely + // rather than walk the table with a no-match filter. + if (rowIds && rowIds.length === 0) return { dispatchId: null } + // Lazy imports: `./service` and `./dispatcher` both close cycles back to + // this module; `@trigger.dev/sdk` is heavy and only needed on this op. + const { getTableById } = await import('./service') const table = await getTableById(tableId) if (!table) throw new Error('Table not found') if (table.workspaceId !== workspaceId) throw new Error('Invalid workspace ID') const allGroups = table.schema.workflowGroups ?? [] const targetGroups = groupIds ? allGroups.filter((g) => groupIds.includes(g.id)) : allGroups - if (targetGroups.length === 0) return { triggered: 0 } - - logger.info( - `[Cascade] [${requestId}] manual run table=${tableId} groups=[${targetGroups.map((g) => g.id).join(',')}] rows=${rowIds ? `[${rowIds.join(',')}]` : 'all'} mode=${mode}` + // Tables with no workflow groups are the majority. Auto-fire callers from + // every row write would otherwise produce error-level log spam on every + // PATCH/insert. Manual run-column callers always pass `groupIds` so they + // can't reach here with an empty target. + if (targetGroups.length === 0) return { dispatchId: null } + const targetGroupIds = targetGroups.map((g) => g.id) + + const { bulkClearWorkflowGroupCells, insertDispatch, runDispatcherToCompletion } = await import( + './dispatcher' ) - const filters = [eq(userTableRows.tableId, tableId), eq(userTableRows.workspaceId, workspaceId)] - if (rowIds && rowIds.length > 0) { - filters.push(inArray(userTableRows.id, rowIds)) - } - const candidateRows = await db - .select({ - id: userTableRows.id, - position: userTableRows.position, - data: userTableRows.data, - executions: userTableRows.executions, - createdAt: userTableRows.createdAt, - updatedAt: userTableRows.updatedAt, - }) - .from(userTableRows) - .where(and(...filters)) - .orderBy(asc(userTableRows.position)) - - if (candidateRows.length === 0) return { triggered: 0 } - - // Per-row: collect eligible groups, build cleared data + executionsPatch. - type Update = { - rowId: string - data: RowData - executionsPatch: Record - } - const updates: Update[] = [] - const clearedRows: TableRow[] = [] - for (const r of candidateRows) { - const tableRow: TableRow = { - id: r.id, - data: r.data as RowData, - executions: (r.executions as RowExecutions) ?? {}, - position: r.position, - createdAt: r.createdAt, - updatedAt: r.updatedAt, - } - const eligibleGroups = targetGroups.filter((g) => - isGroupEligible(g, tableRow, { isManualRun: true, mode }) - ) - if (eligibleGroups.length === 0) continue - - const clearedData: RowData = {} - const executionsPatch: Record = {} - for (const g of eligibleGroups) { - for (const o of g.outputs) clearedData[o.columnName] = null - executionsPatch[g.id] = null + // For manual runs (Run all rows / Run column / Refresh-row / Refresh-cell), + // cancel any prior active dispatches AND in-flight cells in scope before + // clearing. Without this: + // - Two dispatcher loops would walk overlapping rows and burn duplicate work. + // - mode:'all' bulk-clear deletes in-flight sidecar rows without aborting + // workers — those would keep writing into the wiped state. + // Scope: table-wide cancel when rowIds is empty (also cancels active + // dispatches via markActiveDispatchesCancelled), per-row cancel otherwise + // (no dispatch cancel — other rows' dispatches keep running). Dep-edit + // cascade in `updateRow` already cancels its own scope before calling, + // so the duplicate work here is a cheap no-op for that caller. + // Auto-fire (`mode:'new'`) is harmless overlap-wise — the NOT EXISTS + // filter excludes already-attempted rows. + const cancelPriorRuns = isManualRun && (mode === 'all' || mode === 'incomplete') + if (cancelPriorRuns) { + if (!rowIds || rowIds.length === 0) { + await cancelWorkflowGroupRuns(tableId, undefined, { groupIds: targetGroupIds }) + } else { + // Per-row cancel — sequential so we don't fan out N parallel + // markActiveDispatchesCancelled calls (it's a no-op when rowId is set, + // but each call still touches the DB). + for (const rowId of rowIds) { + await cancelWorkflowGroupRuns(tableId, rowId, { groupIds: targetGroupIds }) + } } - updates.push({ rowId: r.id, data: clearedData, executionsPatch }) - - const remainingExec = { ...tableRow.executions } - for (const g of eligibleGroups) delete remainingExec[g.id] - clearedRows.push({ - ...tableRow, - data: { ...tableRow.data, ...clearedData }, - executions: remainingExec, - }) } - if (updates.length === 0) return { triggered: 0 } - - // `skipScheduler: true` because we fire `scheduleRunsForRows` ourselves - // below with `isManualRun: true`. Without the skip, batchUpdateRows runs the - // auto-fire reactor first and any autoRun=true sibling group whose deps are - // satisfied would race the manual call. - await batchUpdateRows({ tableId, updates, workspaceId, skipScheduler: true }, table, requestId) + // Wipe targeted output cols + executions[gid] before any cells fire so the + // user sees the column flip to empty/Pending instantly. + await bulkClearWorkflowGroupCells({ + tableId, + groups: targetGroups.map((g) => ({ id: g.id, outputs: g.outputs })), + rowIds, + mode, + }) - return scheduleRunsForRows(table, clearedRows, { - isManualRun: true, - groupIds: targetGroups.map((g) => g.id), + // Always insert a `table_run_dispatches` row. The dispatcher state machine + // is the single source of truth for cursor advancement, SSE emission, and + // cancel — backend (trigger.dev SaaS vs in-process) only affects how each + // window's cells get executed. + const dispatchId = await insertDispatch({ + tableId, + workspaceId, + requestId, mode, + scope: { + groupIds: targetGroupIds, + ...(rowIds && rowIds.length > 0 ? { rowIds } : {}), + }, + isManualRun, }) + + logger.info( + `[Cascade] [${requestId}] dispatch ${dispatchId} table=${tableId} groups=[${targetGroupIds.join(',')}] rows=${rowIds ? `[${rowIds.join(',')}]` : 'all'} mode=${mode}` + ) + + if (isTriggerDevEnabled) { + // Trigger.dev runs `tableRunDispatcherTask`, which loops `dispatcherStep` + // until done with CRIU-checkpointed waits between windows. + const [{ tableRunDispatcherTask }, { tasks }] = await Promise.all([ + import('@/background/table-run-dispatcher'), + import('@trigger.dev/sdk'), + ]) + await tasks.trigger( + 'table-run-dispatcher', + { dispatchId }, + { concurrencyKey: dispatchId } + ) + } else { + // Local / no-trigger.dev: drive the same loop in-process, fire-and-forget + // so the HTTP request returns instantly (mirrors the trigger.dev path's + // async fan-out). + void runDispatcherToCompletion(dispatchId).catch((err) => + logger.error(`[${requestId}] dispatcher loop failed`, { + dispatchId, + error: toError(err).message, + }) + ) + } + + return { dispatchId } } // ───────────────────────────── Validation ───────────────────────────── diff --git a/packages/db/migrations/0209_smiling_fixer.sql b/packages/db/migrations/0209_smiling_fixer.sql new file mode 100644 index 00000000000..109523397fb --- /dev/null +++ b/packages/db/migrations/0209_smiling_fixer.sql @@ -0,0 +1,41 @@ +CREATE TABLE "table_row_executions" ( + "table_id" text NOT NULL, + "row_id" text NOT NULL, + "group_id" text NOT NULL, + "status" text NOT NULL, + "execution_id" text, + "job_id" text, + "workflow_id" text NOT NULL, + "error" text, + "running_block_ids" text[] DEFAULT '{}'::text[] NOT NULL, + "block_errors" jsonb DEFAULT '{}'::jsonb NOT NULL, + "cancelled_at" timestamp, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "table_row_executions_row_id_group_id_pk" PRIMARY KEY("row_id","group_id") +); +--> statement-breakpoint +CREATE TABLE "table_run_dispatches" ( + "id" text PRIMARY KEY NOT NULL, + "table_id" text NOT NULL, + "workspace_id" text NOT NULL, + "request_id" text NOT NULL, + "mode" text NOT NULL, + "scope" jsonb NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "cursor" integer DEFAULT 0 NOT NULL, + "is_manual_run" boolean DEFAULT true NOT NULL, + "requested_at" timestamp DEFAULT now() NOT NULL, + "completed_at" timestamp, + "cancelled_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "table_row_executions" ADD CONSTRAINT "table_row_executions_table_id_user_table_definitions_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."user_table_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "table_row_executions" ADD CONSTRAINT "table_row_executions_row_id_user_table_rows_id_fk" FOREIGN KEY ("row_id") REFERENCES "public"."user_table_rows"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "table_run_dispatches" ADD CONSTRAINT "table_run_dispatches_table_id_user_table_definitions_id_fk" FOREIGN KEY ("table_id") REFERENCES "public"."user_table_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "table_run_dispatches" ADD CONSTRAINT "table_run_dispatches_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "table_row_executions_table_status_idx" ON "table_row_executions" USING btree ("table_id","status") WHERE "table_row_executions"."status" IN ('queued', 'running', 'pending');--> statement-breakpoint +CREATE INDEX "table_row_executions_execution_id_idx" ON "table_row_executions" USING btree ("execution_id") WHERE "table_row_executions"."execution_id" IS NOT NULL;--> statement-breakpoint +CREATE INDEX "table_row_executions_table_group_idx" ON "table_row_executions" USING btree ("table_id","group_id");--> statement-breakpoint +CREATE INDEX "table_run_dispatches_active_idx" ON "table_run_dispatches" USING btree ("table_id","status");--> statement-breakpoint +CREATE INDEX "table_run_dispatches_watchdog_idx" ON "table_run_dispatches" USING btree ("status","requested_at");--> statement-breakpoint +ALTER TABLE "user_table_rows" DROP COLUMN "executions"; \ No newline at end of file diff --git a/packages/db/migrations/meta/0209_snapshot.json b/packages/db/migrations/meta/0209_snapshot.json new file mode 100644 index 00000000000..378fabd1b3a --- /dev/null +++ b/packages/db/migrations/meta/0209_snapshot.json @@ -0,0 +1,16431 @@ +{ + "id": "77be6741-7a6f-4e72-8be1-54fa58eee8e3", + "prevId": "67dde2dd-485d-4e3d-b2a5-a3caae8b26ae", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_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_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "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": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_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": true + }, + "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 + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_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": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_name_unique": { + "name": "permission_group_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": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "workspace_id": { + "name": "workspace_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_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_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.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "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_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": {} + } + }, + "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": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": 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": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.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.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input" + ] + }, + "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 36023183249..1dd9764d85e 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1457,6 +1457,13 @@ "when": 1778714378630, "tag": "0208_modern_power_man", "breakpoints": true + }, + { + "idx": 209, + "version": "7", + "when": 1779246572978, + "tag": "0209_smiling_fixer", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index ddeab5dd6cf..fe2913d3c89 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -13,6 +13,7 @@ import { jsonb, pgEnum, pgTable, + primaryKey, text, timestamp, uniqueIndex, @@ -2989,13 +2990,6 @@ export const userTableRows = pgTable( .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), data: jsonb('data').notNull(), - /** - * Per-row workflow-group execution state, keyed by `WorkflowGroup.id`. - * Each entry holds status / executionId / jobId / blockErrors / - * runningBlockIds for one group's run on this row. Empty `{}` means no - * group has run for this row yet. - */ - executions: jsonb('executions').notNull().default({}), position: integer('position').notNull().default(0), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -3012,6 +3006,91 @@ export const userTableRows = pgTable( }) ) +/** + * Per-row workflow-group execution state. One row per (rowId, groupId) — the + * group's run metadata (status, executionId, jobId, blockErrors, etc.) for + * one row of one user-defined table. + * + * Lives in a sidecar table (not a JSONB column on `user_table_rows`) so the + * dispatcher and "X running" counter can hit `(table_id, status)` and + * `(table_id, group_id)` indexes directly instead of walking JSONB blobs, and + * so each cell-write rewrites only its own row instead of the whole + * executions object on the parent row tuple. + */ +export const tableRowExecutions = pgTable( + 'table_row_executions', + { + tableId: text('table_id') + .notNull() + .references(() => userTableDefinitions.id, { onDelete: 'cascade' }), + rowId: text('row_id') + .notNull() + .references(() => userTableRows.id, { onDelete: 'cascade' }), + groupId: text('group_id').notNull(), + status: text('status').notNull(), + executionId: text('execution_id'), + jobId: text('job_id'), + workflowId: text('workflow_id').notNull(), + error: text('error'), + runningBlockIds: text('running_block_ids').array().notNull().default(sql`'{}'::text[]`), + blockErrors: jsonb('block_errors').notNull().default({}), + cancelledAt: timestamp('cancelled_at'), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + pk: primaryKey({ columns: [table.rowId, table.groupId] }), + tableStatusInFlightIdx: index('table_row_executions_table_status_idx') + .on(table.tableId, table.status) + .where(sql`${table.status} IN ('queued', 'running', 'pending')`), + executionIdIdx: index('table_row_executions_execution_id_idx') + .on(table.executionId) + .where(sql`${table.executionId} IS NOT NULL`), + tableGroupIdx: index('table_row_executions_table_group_idx').on(table.tableId, table.groupId), + }) +) + +/** + * One row per "Run column / Run row / Run all rows" gesture on a user table. + * The dispatcher task walks the table in row-position windows, advancing + * `cursor` as it enqueues cells into trigger.dev. Cancel flips `status` to + * `'cancelled'` in one write; the dispatcher bails at the next iteration and + * a bulk-SQL cell-cancel sweep neuters anything still in trigger.dev's queue + * (workers no-op on pickup via the cancel-sticky guard). + */ +export const tableRunDispatches = pgTable( + 'table_run_dispatches', + { + id: text('id').primaryKey(), + tableId: text('table_id') + .notNull() + .references(() => userTableDefinitions.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + requestId: text('request_id').notNull(), + /** `'all'` re-runs completed cells; `'incomplete'` skips them. */ + mode: text('mode').notNull(), + /** `{ groupIds: string[], rowIds?: string[] }` — the run's scope. */ + scope: jsonb('scope').notNull(), + /** `pending` → `dispatching` → `complete` | `cancelled`. */ + status: text('status').notNull().default('pending'), + /** Highest `user_table_rows.position` we've already enqueued cells for. */ + cursor: integer('cursor').notNull().default(0), + /** When true, eligibility bypasses `autoRun: false` skip and treats + * terminal states as re-runnable. Auto-fire paths (row inserts, + * CSV import, addWorkflowGroup) set this to false so the dispatch + * honors the autoRun toggle. */ + isManualRun: boolean('is_manual_run').notNull().default(true), + requestedAt: timestamp('requested_at').notNull().defaultNow(), + completedAt: timestamp('completed_at'), + cancelledAt: timestamp('cancelled_at'), + }, + (table) => ({ + activeIdx: index('table_run_dispatches_active_idx').on(table.tableId, table.status), + watchdogIdx: index('table_run_dispatches_watchdog_idx').on(table.status, table.requestedAt), + }) +) + export const oauthApplication = pgTable( 'oauth_application', { diff --git a/packages/testing/src/mocks/database.mock.ts b/packages/testing/src/mocks/database.mock.ts index b73eb3566e7..98dda999745 100644 --- a/packages/testing/src/mocks/database.mock.ts +++ b/packages/testing/src/mocks/database.mock.ts @@ -115,7 +115,18 @@ const forClause = vi.fn(forBuilder) const onConflictDoUpdate = vi.fn(() => ({ returning }) as unknown as Promise) const onConflictDoNothing = vi.fn(() => ({ returning }) as unknown as Promise) -const whereBuilder = () => ({ limit, orderBy, returning, groupBy, for: forClause }) +const whereBuilder = () => { + // Some call sites (e.g. `db.select().from(t).where(eq(...))` with no + // limit/orderBy) await the where directly. Make the builder a thenable so + // those calls resolve to the default empty array. + const thenable: any = Promise.resolve([] as unknown[]) + thenable.limit = limit + thenable.orderBy = orderBy + thenable.returning = returning + thenable.groupBy = groupBy + thenable.for = forClause + return thenable +} const where = vi.fn(whereBuilder) const joinBuilder = (): { where: typeof where; innerJoin: any; leftJoin: any } => ({ diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index ba91d9d7746..66e14fabe0c 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1141,6 +1141,34 @@ export const schemaMock = { updatedAt: 'updatedAt', createdBy: 'createdBy', }, + tableRowExecutions: { + tableId: 'tableId', + rowId: 'rowId', + groupId: 'groupId', + status: 'status', + executionId: 'executionId', + jobId: 'jobId', + workflowId: 'workflowId', + error: 'error', + runningBlockIds: 'runningBlockIds', + blockErrors: 'blockErrors', + cancelledAt: 'cancelledAt', + updatedAt: 'updatedAt', + }, + tableRunDispatches: { + id: 'id', + tableId: 'tableId', + workspaceId: 'workspaceId', + requestId: 'requestId', + mode: 'mode', + scope: 'scope', + status: 'status', + cursor: 'cursor', + isManualRun: 'isManualRun', + requestedAt: 'requestedAt', + completedAt: 'completedAt', + cancelledAt: 'cancelledAt', + }, oauthApplication: { id: 'id', name: 'name', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 8bd93e9545a..3f8b4f77c87 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 746, - zodRoutes: 746, + totalRoutes: 747, + zodRoutes: 747, nonZodRoutes: 0, } as const From d9dd7a3e55ce6a33706ef129471c42545831cd43 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 20 May 2026 10:55:26 -0700 Subject: [PATCH 02/16] fix(cors): re-enable credentials on chat/form embed CORS policy (#4673) * fix(cors): re-enable credentials on embed CORS policy Chat and form embeds authenticate via the chat_auth_ / form auth cookie set by setDeploymentAuthCookie. The previous PR set Access-Control-Allow-Credentials: false on these routes, which made the browser drop the auth cookie and produce 401s on subsequent embed calls after login. Restore credentials: true (matching pre-consolidation behavior) while keeping reflected origin and Vary: Origin. The wildcard fallback when Origin is absent now also drops credentials to stay CORS-spec-compliant. Co-Authored-By: Claude Opus 4.7 * chore(cors): trim verbose comments in proxy Co-Authored-By: Claude Opus 4.7 * chore(cors): restore concise TSDoc on proxy CORS helpers Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- apps/sim/proxy.test.ts | 16 ++++++++------- apps/sim/proxy.ts | 45 ++++++++++++------------------------------ 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/apps/sim/proxy.test.ts b/apps/sim/proxy.test.ts index 0282d670cd3..9311b146358 100644 --- a/apps/sim/proxy.test.ts +++ b/apps/sim/proxy.test.ts @@ -44,7 +44,7 @@ describe('resolveApiCorsPolicy', () => { expect(policy.headers).toContain('X-API-Key') }) - it('reflects origin for chat and form embeds, never sets credentials', () => { + it('reflects origin for chat and form embeds with credentials enabled', () => { const paths = [ '/api/chat/abc', '/api/chat/abc/otp', @@ -56,13 +56,19 @@ describe('resolveApiCorsPolicy', () => { const policy = resolveApiCorsPolicy(makeRequest(path, 'https://customer.example')) expect(policy).toEqual({ origin: 'https://customer.example', - credentials: false, + credentials: true, methods: 'GET, POST, PUT, OPTIONS', headers: 'Content-Type, X-Requested-With', }) } }) + it('drops credentials on embed policy when Origin header is absent (CORS spec invariant)', () => { + const policy = resolveApiCorsPolicy(makeRequest('/api/chat/abc')) + expect(policy.origin).toBe('*') + expect(policy.credentials).toBe(false) + }) + it('allows PUT on the embed policy (used by OTP verification on /[identifier]/otp)', () => { const policy = resolveApiCorsPolicy( makeRequest('/api/chat/abc/otp', 'https://customer.example') @@ -70,16 +76,12 @@ describe('resolveApiCorsPolicy', () => { expect(policy.methods).toContain('PUT') }) - it('falls back to wildcard for chat/form embeds when no origin header is present', () => { - expect(resolveApiCorsPolicy(makeRequest('/api/chat/abc')).origin).toBe('*') - }) - it('applies the embed policy to future identifier subroutes (not just /otp, /sso)', () => { const policy = resolveApiCorsPolicy( makeRequest('/api/chat/abc/transcript', 'https://customer.example') ) expect(policy.origin).toBe('https://customer.example') - expect(policy.credentials).toBe(false) + expect(policy.credentials).toBe(true) }) it('uses the default credentialed policy for workspace-internal chat/form routes', () => { diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index a5b8ea6df90..1bf1ccac505 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -22,19 +22,10 @@ const DEFAULT_API_ALLOWED_HEADERS = const WORKFLOW_EXECUTE_HEADERS = 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key' -/** - * Workspace-internal segments under /api/{chat,form}/* that must NOT - * receive the embed policy. They serve the workspace UI with session - * cookies and need the default credentialed policy. - */ +/** Subpaths under /api/{chat,form}/* that serve the workspace UI, not embeds. */ const EMBED_RESERVED_SEGMENTS = new Set(['manage', 'validate']) -/** - * True for /api/{chat,form}/[identifier] and any deeper subroute - * (e.g. /otp, /sso). The identifier segment is explicitly checked - * against EMBED_RESERVED_SEGMENTS so workspace-internal routes fall - * through to the default credentialed policy. - */ +/** True for /api/{chat,form}/[identifier] and any deeper subroute. */ function isEmbedPath(pathname: string): boolean { const segments = pathname.split('/') if (segments.length < 4) return false @@ -79,20 +70,16 @@ const CORS_RULES: readonly CorsRule[] = [ }), }, { - // Embed endpoints: /api/chat/[identifier] and /api/form/[identifier] - // (plus their /otp and /sso subroutes). These run on customer domains — - // reflect the request origin and omit credentials (auth uses signed - // tokens, not cookies). Workspace-internal subpaths (`manage`, `validate`, - // and the bare collection routes) are deliberately excluded so they - // continue to receive the default credentialed policy. match: (p) => isEmbedPath(p), - policy: (request) => ({ - origin: request.headers.get('origin') || '*', - credentials: false, - // PUT is required for OTP verification on /[identifier]/otp. - methods: 'GET, POST, PUT, OPTIONS', - headers: 'Content-Type, X-Requested-With', - }), + policy: (request) => { + const requestOrigin = request.headers.get('origin') + return { + origin: requestOrigin || '*', + credentials: !!requestOrigin, + methods: 'GET, POST, PUT, OPTIONS', + headers: 'Content-Type, X-Requested-With', + } + }, }, { match: (p) => /^\/api\/workflows\/[^/]+\/execute$/.test(p), @@ -105,10 +92,7 @@ const CORS_RULES: readonly CorsRule[] = [ }, ] -/** - * Single source of truth for CORS on /api/* — next.config.ts headers are - * baked at build time and would freeze NEXT_PUBLIC_APP_URL into the image. - */ +/** Single source of truth for /api/* CORS — resolved at request time, not baked at build. */ export function resolveApiCorsPolicy(request: NextRequest): CorsPolicy { const { pathname } = request.nextUrl for (const rule of CORS_RULES) { @@ -134,10 +118,7 @@ function applyCorsHeaders(response: NextResponse, policy: CorsPolicy): void { } } -/** - * Short-circuit preflight: Next's auto-OPTIONS for route handlers without - * an explicit OPTIONS export does not carry middleware headers. - */ +/** Next's auto-OPTIONS doesn't carry middleware headers, so we answer preflight here. */ function buildPreflightResponse(policy: CorsPolicy): NextResponse { const response = new NextResponse(null, { status: 204 }) applyCorsHeaders(response, policy) From 46db40620ff84e2e42e7a20b74ff8b515d11c5fe Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 20 May 2026 12:10:04 -0700 Subject: [PATCH 03/16] feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers (#4441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mcp): OAuth 2.1 support for outbound MCP servers * fix(mcp): tighten OAuth refresh race and session-error detection Re-load the OAuth row inside withMcpOauthRefreshLock so concurrent callers observe predecessor-written tokens instead of a stale snapshot loaded before lock acquisition. Without this, the second caller's provider held a rotated-out refresh token and the SDK tripped invalid_grant, forcing reauthorization. Switch isSessionError to match the SDK's typed StreamableHTTPError (code 404/400) instead of substring-checking arbitrary error messages, removing false positives on URLs that happen to contain those digits. Co-Authored-By: Claude Opus 4.7 * refactor(mcp): tighten OAuth callback contract and registration metadata - Validate callback query params via mcpOauthCallbackContract instead of raw searchParams.get, matching the rest of the MCP route surface. - Drop non-RFC-7591 application_type field from dynamic client registration to avoid rejection by strict authorization servers. - Collapse the pre-lock OAuth row load in createClient — the row is now loaded exclusively inside withMcpOauthRefreshLock, removing a redundant query and a stale-snapshot path. * fix(mcp): narrow workspaceId before async closure in OAuth createClient * fix(mcp): return authType from create-server endpoint The POST /api/mcp/servers handler omitted authType from the success response, so useCreateMcpServer always saw data.data.authType as undefined and never triggered the OAuth popup after creating an OAuth-protected server. Thread authType through performCreateMcpServer into the response so the client can decide whether to auto-start OAuth. * fix(mcp): mirror server null normalization in optimistic oauthClientId update Co-Authored-By: Claude Opus 4.7 * fix(mcp): revert optimistic oauthClientId to undefined to match McpServer type The response contract preprocesses null → undefined, so McpServer.oauthClientId is string | undefined. Using null broke type checking. Co-Authored-By: Claude Opus 4.7 * fix(mcp): tighten OAuth probe signal and clear stale popup interval - probe: only classify as OAuth on resource_metadata or scope params. Bare `Bearer error="invalid_token"` is generic and used by API-key servers, so it must not auto-flip the auth type to OAuth. - popup hook: clear any existing close-watcher interval before overwriting when startOauthForServer is invoked twice for the same serverId. Co-Authored-By: Claude Opus 4.7 * fix(mcp): normalize empty-string oauthClientId at route boundary Orchestration already converts falsy → null via `|| null` (server-lifecycle.ts), so the DB was never receiving an empty string. Tightening the route layer to match the same convention keeps the boundary contract consistent and avoids relying on downstream normalization. Co-Authored-By: Claude Opus 4.7 * feat(canvas): expand MCP tool params into per-row labels on block tile The MCP Tool block on the workflow canvas previously crammed every selected- tool parameter into a stringified blob under the `Tool` row. Now, when a tool is selected, the tile reads the cached `_toolSchema` and emits one labeled SubBlockRow per parameter (matching the Exa block's per-param layout). Labels reuse `formatParameterLabel` for parity with the editor panel; values pass through the existing `getDisplayValue` so booleans/numbers/arrays render identically to other blocks. Deterministic tile height counts expanded rows so the tile sizes correctly. Co-Authored-By: Claude Opus 4.7 * feat(logs): show MCP icon and strip prefix in trace tool spans Tool spans for MCP calls were rendering the raw id (e.g. `mcp-f908f259-planetscale_list_organizations`) with the default blank- square icon. Now they read just the tool name and render the MCP block's icon and bgColor, matching how workflow-execute tools render. Co-Authored-By: Claude Opus 4.7 * fix(logs): lift near-black trace icon backgrounds for dark-mode contrast Block bgColors below a small luminance threshold (e.g. the MCP block's #181C1E) rendered nearly invisible against the dark-mode surface (--bg: #1b1b1b). Adds a tiny adjustBgForContrast helper that floors each RGB channel at 0x33 only when luminance is below 30,000, leaving every branded color above that band untouched. Applied to both the trace tree row and the detail pane. Co-Authored-By: Claude Opus 4.7 * fix(logs): fall back to neutral gray for near-black trace icon bgs #333333 was still too close to the dark-mode surface to read. For bgs below the luminance threshold (e.g. the MCP block's #181C1E) we now fall back to DEFAULT_BLOCK_COLOR (#6b7280) — the same neutral the renderer uses for blocks with no distinct identity. Clearly visible in both themes; brighter brand colors still pass through. Co-Authored-By: Claude Opus 4.7 * chore(db): drop 0209_mcp_oauth migration ahead of staging merge Staging shipped 0209_smiling_fixer; the MCP OAuth migration will be regenerated on top of staging as 0210. Co-Authored-By: Claude Opus 4.7 * chore(db): regenerate MCP OAuth migration as 0210 Re-runs drizzle-kit generate on top of staging's 0209_smiling_fixer. Same schema (mcp_server_oauth table + mcp_servers.auth_type / oauth_* columns) as the dropped 0209_mcp_oauth. Co-Authored-By: Claude Opus 4.7 * chore(audit): bump route baseline 748 → 749 after staging merge The post-merge route count is 749 (this branch's OAuth start/callback plus staging's new route). I had set the baseline to 748 in the merge conflict resolution — bumping to match reality so the strict audit passes. Co-Authored-By: Claude Opus 4.7 * chore: remove source-command skill files committed by accident These were untracked-then-accidentally-staged in 05c4bc19e via a wide `git add -A`. They aren't part of this PR's scope. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- apps/sim/app/api/mcp/oauth/callback/route.ts | 181 + .../sim/app/api/mcp/oauth/start/route.test.ts | 137 + apps/sim/app/api/mcp/oauth/start/route.ts | 122 + apps/sim/app/api/mcp/servers/[id]/route.ts | 29 +- apps/sim/app/api/mcp/servers/route.ts | 23 +- apps/sim/app/api/mcp/tools/discover/route.ts | 15 +- apps/sim/app/api/mcp/tools/execute/route.ts | 52 +- .../components/trace-view/trace-view.tsx | 30 +- .../logs/components/log-details/utils.ts | 24 +- .../mcp/components/form-field/form-field.tsx | 2 +- .../mcp-server-form-modal.tsx | 342 +- .../settings/components/mcp/mcp.tsx | 398 +- .../components/tool-input/tool-input.tsx | 12 +- .../workflow-block/workflow-block.tsx | 54 +- apps/sim/hooks/mcp/use-mcp-oauth-popup.ts | 132 + apps/sim/hooks/mcp/use-mcp-tools.ts | 11 +- apps/sim/hooks/queries/mcp.ts | 164 +- apps/sim/lib/api/contracts/mcp.ts | 77 + apps/sim/lib/core/utils/urls.ts | 11 +- apps/sim/lib/mcp/client.test.ts | 25 +- apps/sim/lib/mcp/client.ts | 19 +- apps/sim/lib/mcp/connection-manager.test.ts | 65 +- apps/sim/lib/mcp/connection-manager.ts | 131 +- apps/sim/lib/mcp/middleware.ts | 2 +- apps/sim/lib/mcp/oauth/callback-reasons.ts | 23 + apps/sim/lib/mcp/oauth/creds-diff.ts | 39 + apps/sim/lib/mcp/oauth/index.ts | 30 + apps/sim/lib/mcp/oauth/probe.ts | 92 + apps/sim/lib/mcp/oauth/provider.ts | 179 + apps/sim/lib/mcp/oauth/revoke.ts | 104 + apps/sim/lib/mcp/oauth/storage.test.ts | 94 + apps/sim/lib/mcp/oauth/storage.ts | 249 + apps/sim/lib/mcp/oauth/url-validation.ts | 25 + .../lib/mcp/orchestration/server-lifecycle.ts | 178 +- apps/sim/lib/mcp/service.ts | 141 +- apps/sim/lib/mcp/types.ts | 38 + apps/sim/lib/mcp/workflow-tool-schema.ts | 4 +- packages/db/migrations/0210_mcp_oauth.sql | 23 + .../db/migrations/meta/0210_snapshot.json | 16596 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 62 + packages/testing/src/mocks/index.ts | 7 + packages/testing/src/mocks/mcp-oauth.mock.ts | 74 + packages/testing/src/mocks/schema.mock.ts | 13 + scripts/check-api-validation-contracts.ts | 8 +- 45 files changed, 19496 insertions(+), 548 deletions(-) create mode 100644 apps/sim/app/api/mcp/oauth/callback/route.ts create mode 100644 apps/sim/app/api/mcp/oauth/start/route.test.ts create mode 100644 apps/sim/app/api/mcp/oauth/start/route.ts create mode 100644 apps/sim/hooks/mcp/use-mcp-oauth-popup.ts create mode 100644 apps/sim/lib/mcp/oauth/callback-reasons.ts create mode 100644 apps/sim/lib/mcp/oauth/creds-diff.ts create mode 100644 apps/sim/lib/mcp/oauth/index.ts create mode 100644 apps/sim/lib/mcp/oauth/probe.ts create mode 100644 apps/sim/lib/mcp/oauth/provider.ts create mode 100644 apps/sim/lib/mcp/oauth/revoke.ts create mode 100644 apps/sim/lib/mcp/oauth/storage.test.ts create mode 100644 apps/sim/lib/mcp/oauth/storage.ts create mode 100644 apps/sim/lib/mcp/oauth/url-validation.ts create mode 100644 packages/db/migrations/0210_mcp_oauth.sql create mode 100644 packages/db/migrations/meta/0210_snapshot.json create mode 100644 packages/testing/src/mocks/mcp-oauth.mock.ts diff --git a/apps/sim/app/api/mcp/oauth/callback/route.ts b/apps/sim/app/api/mcp/oauth/callback/route.ts new file mode 100644 index 00000000000..721675dac9d --- /dev/null +++ b/apps/sim/app/api/mcp/oauth/callback/route.ts @@ -0,0 +1,181 @@ +import { auth as mcpAuth } from '@modelcontextprotocol/sdk/client/auth.js' +import { db } from '@sim/db' +import { mcpServers } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { and, eq, isNull } from 'drizzle-orm' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { mcpOauthCallbackContract } from '@/lib/api/contracts/mcp' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + assertSafeOauthServerUrl, + clearState, + clearVerifier, + loadOauthRowByState, + loadPreregisteredClient, + type McpOauthCallbackReason, + SimMcpOauthProvider, +} from '@/lib/mcp/oauth' +import { mcpService } from '@/lib/mcp/service' + +const logger = createLogger('McpOauthCallbackAPI') + +export const dynamic = 'force-dynamic' + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function jsonLiteral(value: string | undefined): string { + if (value === undefined) return 'undefined' + return JSON.stringify(value).replace(//g, '\\u003e') +} + +function htmlClose( + message: string, + ok: boolean, + reason: McpOauthCallbackReason, + serverId?: string +): NextResponse { + const safeMessage = escapeHtml(message) + const title = ok ? 'Connected' : 'Connection failed' + const body = `${title}

${safeMessage}

` + return new NextResponse(body, { + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }) +} + +export const GET = withRouteHandler(async (request: NextRequest) => { + const parsed = await parseRequest(mcpOauthCallbackContract, request, {}) + if (!parsed.success) { + return htmlClose('Malformed authorization callback.', false, 'missing_params') + } + const { state, code, error: errorParam } = parsed.data.query + + const initialRow = state ? await loadOauthRowByState(state).catch(() => null) : null + const stateRowServerId = initialRow?.mcpServerId + + if (errorParam) { + logger.warn(`MCP OAuth callback received error: ${errorParam}`) + if (initialRow) await clearState(initialRow.id).catch(() => {}) + return htmlClose( + `Authorization failed: ${errorParam}`, + false, + 'provider_error', + stateRowServerId + ) + } + if (!state || !code) { + return htmlClose( + 'Missing state or code in callback URL.', + false, + 'missing_params', + stateRowServerId + ) + } + + let serverId: string | undefined + try { + const session = await getSession() + if (!session?.user?.id) { + return htmlClose( + 'You must be signed in to complete authorization.', + false, + 'unauthenticated', + stateRowServerId + ) + } + + const row = initialRow + if (!row) { + return htmlClose('Invalid or expired authorization state.', false, 'invalid_state') + } + serverId = row.mcpServerId + + if (session.user.id !== row.userId) { + return htmlClose( + 'You must be signed in as the same user that initiated the flow.', + false, + 'user_mismatch', + serverId + ) + } + + const [server] = await db + .select({ id: mcpServers.id, url: mcpServers.url, workspaceId: mcpServers.workspaceId }) + .from(mcpServers) + .where(and(eq(mcpServers.id, row.mcpServerId), isNull(mcpServers.deletedAt))) + .limit(1) + if (!server || !server.url) { + return htmlClose('Server no longer exists.', false, 'server_gone', serverId) + } + if (server.workspaceId !== row.workspaceId) { + return htmlClose( + 'Workspace mismatch on authorization callback.', + false, + 'invalid_state', + serverId + ) + } + try { + assertSafeOauthServerUrl(server.url) + } catch { + return htmlClose( + 'MCP OAuth requires https (or http://localhost for development).', + false, + 'insecure_url', + serverId + ) + } + + // Burn state before token exchange so a replayed callback cannot reuse it. + await clearState(row.id) + + const preregistered = await loadPreregisteredClient(server.id) + const provider = new SimMcpOauthProvider({ row, preregistered }) + let result: Awaited> + try { + result = await mcpAuth(provider, { + serverUrl: server.url, + authorizationCode: code, + }) + } catch (e) { + logger.error('Token exchange failed during MCP OAuth callback', e) + return htmlClose( + 'Token exchange failed. Please try again.', + false, + 'token_exchange_failed', + server.id + ) + } finally { + await clearVerifier(row.id) + } + + if (result !== 'AUTHORIZED') { + return htmlClose('Authorization did not complete.', false, 'token_exchange_failed', server.id) + } + + try { + await mcpService.clearCache(server.workspaceId) + await mcpService.discoverServerTools(session.user.id, server.id, server.workspaceId) + } catch (e) { + logger.warn('Post-auth tools refresh failed', toError(e).message) + } + + return htmlClose('Connected. You can close this window.', true, 'authorized', server.id) + } catch (error) { + logger.error('MCP OAuth callback failed', error) + return htmlClose('Authorization failed. Please try again.', false, 'unknown', serverId) + } +}) diff --git a/apps/sim/app/api/mcp/oauth/start/route.test.ts b/apps/sim/app/api/mcp/oauth/start/route.test.ts new file mode 100644 index 00000000000..7c81138ca42 --- /dev/null +++ b/apps/sim/app/api/mcp/oauth/start/route.test.ts @@ -0,0 +1,137 @@ +/** + * @vitest-environment node + */ +import { + dbChainMock, + dbChainMockFns, + hybridAuthMock, + hybridAuthMockFns, + McpOauthRedirectRequiredMock, + mcpOauthMock, + mcpOauthMockFns, + permissionsMock, + permissionsMockFns, + resetDbChainMock, + schemaMock, +} from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockMcpAuth } = vi.hoisted(() => ({ + mockMcpAuth: vi.fn(), +})) + +vi.mock('@sim/db', () => dbChainMock) +vi.mock('@sim/db/schema', () => schemaMock) +vi.mock('drizzle-orm', () => ({ + and: vi.fn(), + eq: vi.fn(), + isNull: vi.fn(), +})) +vi.mock('@modelcontextprotocol/sdk/client/auth.js', () => ({ + auth: mockMcpAuth, +})) +vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) +vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) +vi.mock('@/lib/mcp/oauth', () => mcpOauthMock) + +import { GET } from './route' + +describe('MCP OAuth start route', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-2', + userName: 'User Two', + userEmail: 'user2@example.com', + authType: 'session', + }) + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write') + dbChainMockFns.limit.mockResolvedValue([ + { + id: 'server-1', + name: 'Exa', + url: 'https://mcp.exa.ai/mcp', + workspaceId: 'workspace-1', + authType: 'oauth', + deletedAt: null, + }, + ]) + mcpOauthMockFns.mockGetOrCreateOauthRow.mockResolvedValue({ + id: 'oauth-row-1', + mcpServerId: 'server-1', + userId: 'user-1', + workspaceId: 'workspace-1', + clientInformation: null, + tokens: null, + codeVerifier: null, + state: null, + stateCreatedAt: null, + updatedAt: new Date(), + }) + mcpOauthMockFns.mockLoadPreregisteredClient.mockResolvedValue(undefined) + mockMcpAuth.mockRejectedValue(new McpOauthRedirectRequiredMock('https://mcp.exa.ai/authorize')) + }) + + it('requires workspace write permission via MCP auth middleware', async () => { + const request = new NextRequest( + 'http://localhost:3000/api/mcp/oauth/start?workspaceId=workspace-1&serverId=server-1' + ) + + await GET(request) + + expect(permissionsMockFns.mockGetUserEntityPermissions).toHaveBeenCalledWith( + 'user-2', + 'workspace', + 'workspace-1' + ) + }) + + it('uses a workspace-scoped OAuth row and stamps the latest authorizing user', async () => { + const request = new NextRequest( + 'http://localhost:3000/api/mcp/oauth/start?workspaceId=workspace-1&serverId=server-1' + ) + + const response = await GET(request) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toEqual({ + status: 'redirect', + authorizationUrl: 'https://mcp.exa.ai/authorize', + }) + expect(mcpOauthMockFns.mockGetOrCreateOauthRow).toHaveBeenCalledWith({ + mcpServerId: 'server-1', + userId: 'user-2', + workspaceId: 'workspace-1', + }) + expect(mcpOauthMockFns.mockSetOauthRowUser).toHaveBeenCalledWith('oauth-row-1', 'user-2') + }) + + it('rejects a second user starting OAuth while another authorization is active', async () => { + mcpOauthMockFns.mockGetOrCreateOauthRow.mockResolvedValueOnce({ + id: 'oauth-row-1', + mcpServerId: 'server-1', + userId: 'user-1', + workspaceId: 'workspace-1', + clientInformation: null, + tokens: null, + codeVerifier: null, + state: 'hashed-active-state', + stateCreatedAt: new Date(), + updatedAt: new Date(), + }) + const request = new NextRequest( + 'http://localhost:3000/api/mcp/oauth/start?workspaceId=workspace-1&serverId=server-1' + ) + + const response = await GET(request) + const body = await response.json() + + expect(response.status).toBe(409) + expect(body.error).toBe('OAuth authorization already in progress for this server') + expect(mockMcpAuth).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/mcp/oauth/start/route.ts b/apps/sim/app/api/mcp/oauth/start/route.ts new file mode 100644 index 00000000000..55a7d41956f --- /dev/null +++ b/apps/sim/app/api/mcp/oauth/start/route.ts @@ -0,0 +1,122 @@ +import { auth as mcpAuth } from '@modelcontextprotocol/sdk/client/auth.js' +import { db } from '@sim/db' +import { mcpServers } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { and, eq, isNull } from 'drizzle-orm' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { startMcpOauthContract } from '@/lib/api/contracts/mcp' +import { parseRequest } from '@/lib/api/server' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { withMcpAuth } from '@/lib/mcp/middleware' +import { + assertSafeOauthServerUrl, + getOrCreateOauthRow, + loadPreregisteredClient, + McpOauthInsecureUrlError, + McpOauthRedirectRequired, + SimMcpOauthProvider, + setOauthRowUser, +} from '@/lib/mcp/oauth' +import { createMcpErrorResponse } from '@/lib/mcp/utils' + +const logger = createLogger('McpOauthStartAPI') +const OAUTH_START_TTL_MS = 10 * 60 * 1000 + +export const dynamic = 'force-dynamic' + +export const GET = withRouteHandler( + withMcpAuth('write')(async (request: NextRequest, { userId, workspaceId }) => { + try { + const parsed = await parseRequest(startMcpOauthContract, request, {}) + if (!parsed.success) return parsed.response + const { serverId } = parsed.data.query + + const [server] = await db + .select() + .from(mcpServers) + .where( + and( + eq(mcpServers.id, serverId), + eq(mcpServers.workspaceId, workspaceId), + isNull(mcpServers.deletedAt) + ) + ) + .limit(1) + + if (!server) { + return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404) + } + if (server.authType !== 'oauth') { + return createMcpErrorResponse( + new Error(`Server authType is "${server.authType}", not oauth`), + 'Server is not configured for OAuth', + 400 + ) + } + if (!server.url) { + return createMcpErrorResponse(new Error('Server has no URL'), 'Missing server URL', 400) + } + try { + assertSafeOauthServerUrl(server.url) + } catch (e) { + if (e instanceof McpOauthInsecureUrlError) { + return createMcpErrorResponse( + e, + 'MCP OAuth requires https (or http://localhost for development)', + 400 + ) + } + throw e + } + + const row = await getOrCreateOauthRow({ + mcpServerId: server.id, + userId, + workspaceId, + }) + const hasActiveFlow = + !!row.state && + !!row.stateCreatedAt && + row.stateCreatedAt.getTime() > Date.now() - OAUTH_START_TTL_MS + if (hasActiveFlow && row.userId && row.userId !== userId) { + return createMcpErrorResponse( + new Error('OAuth authorization already in progress'), + 'OAuth authorization already in progress for this server', + 409 + ) + } + if (row.userId !== userId) { + await setOauthRowUser(row.id, userId) + row.userId = userId + } + const preregistered = await loadPreregisteredClient(server.id) + const provider = new SimMcpOauthProvider({ row, preregistered }) + + try { + const result = await mcpAuth(provider, { serverUrl: server.url }) + if (result === 'AUTHORIZED') { + return NextResponse.json({ status: 'already_authorized' }) + } + return createMcpErrorResponse( + new Error('Provider did not capture redirect URL'), + 'Failed to start OAuth flow', + 500 + ) + } catch (e) { + if (e instanceof McpOauthRedirectRequired) { + logger.info(`OAuth redirect for server ${serverId}`) + return NextResponse.json({ + status: 'redirect', + authorizationUrl: e.authorizationUrl, + }) + } + throw e + } + } catch (error) { + logger.error('Error starting MCP OAuth flow:', error) + return createMcpErrorResponse(toError(error), 'Failed to start OAuth flow', 500) + } + }) +) diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts index 6795f6383e1..4242fdef119 100644 --- a/apps/sim/app/api/mcp/servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/route.ts @@ -45,23 +45,25 @@ export const PATCH = withRouteHandler( } ) - // Remove workspaceId from body to prevent it from being updated - const { workspaceId: _, ...updateData } = body - const result = await performUpdateMcpServer({ workspaceId, userId, actorName: userName, actorEmail: userEmail, serverId, - name: updateData.name, - description: updateData.description, - transport: updateData.transport, - url: updateData.url, - headers: updateData.headers, - timeout: updateData.timeout, - retries: updateData.retries, - enabled: updateData.enabled, + name: body.name, + description: body.description, + transport: body.transport, + url: body.url, + headers: body.headers, + timeout: body.timeout, + retries: body.retries, + enabled: body.enabled, + authType: body.authType, + oauthClientId: body.oauthClientId || null, + oauthClientIdProvided: body.oauthClientId !== undefined, + oauthClientSecret: body.oauthClientSecret, + oauthClientSecretProvided: body.oauthClientSecret !== undefined, request, }) if (!result.success || !result.server) { @@ -75,7 +77,10 @@ export const PATCH = withRouteHandler( logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`) - return createMcpSuccessResponse({ server: updatedServer }) + const { oauthClientSecret: _secret, ...rest } = updatedServer + return createMcpSuccessResponse({ + server: { ...rest, hasOauthClientSecret: !!_secret }, + }) } catch (error) { logger.error(`[${requestId}] Error updating MCP server:`, error) return createMcpErrorResponse(toError(error), 'Failed to update MCP server', 500) diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts index f0f2744b053..1d02caeef74 100644 --- a/apps/sim/app/api/mcp/servers/route.ts +++ b/apps/sim/app/api/mcp/servers/route.ts @@ -27,11 +27,16 @@ export const GET = withRouteHandler( try { logger.info(`[${requestId}] Listing MCP servers for workspace ${workspaceId}`) - const servers = await db + const rows = await db .select() .from(mcpServers) .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) + const servers = rows.map(({ oauthClientSecret: _secret, ...rest }) => ({ + ...rest, + hasOauthClientSecret: !!_secret, + })) + logger.info( `[${requestId}] Listed ${servers.length} MCP servers for workspace ${workspaceId}` ) @@ -45,13 +50,6 @@ export const GET = withRouteHandler( /** * POST - Register a new MCP server for the workspace (requires write permission) - * - * Uses deterministic server IDs based on URL hash to ensure that re-adding - * the same server produces the same ID. This prevents "server not found" errors - * when workflows reference the old server ID after delete/re-add cycles. - * - * If a server with the same ID already exists (same URL in same workspace), - * it will be updated instead of creating a duplicate. */ export const POST = withRouteHandler( withMcpAuth('write')( @@ -96,6 +94,11 @@ export const POST = withRouteHandler( retries: body.retries, enabled: body.enabled, source, + authType: body.authType, + oauthClientId: body.oauthClientId || null, + oauthClientIdProvided: body.oauthClientId !== undefined, + oauthClientSecret: body.oauthClientSecret, + oauthClientSecretProvided: body.oauthClientSecret !== undefined, request, }) if (!result.success || !result.serverId) { @@ -112,8 +115,8 @@ export const POST = withRouteHandler( return createMcpSuccessResponse( result.updated - ? { serverId: result.serverId, updated: true } - : { serverId: result.serverId }, + ? { serverId: result.serverId, updated: true, authType: result.authType } + : { serverId: result.serverId, authType: result.authType }, result.updated ? 200 : 201 ) } catch (error) { diff --git a/apps/sim/app/api/mcp/tools/discover/route.ts b/apps/sim/app/api/mcp/tools/discover/route.ts index e94f2f56328..b125fa7ff2b 100644 --- a/apps/sim/app/api/mcp/tools/discover/route.ts +++ b/apps/sim/app/api/mcp/tools/discover/route.ts @@ -1,3 +1,4 @@ +import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { mcpToolDiscoveryQuerySchema, refreshMcpToolsBodySchema } from '@/lib/api/contracts/mcp' @@ -5,7 +6,7 @@ import { validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpService } from '@/lib/mcp/service' -import type { McpToolDiscoveryResponse } from '@/lib/mcp/types' +import { McpOauthAuthorizationRequiredError, type McpToolDiscoveryResponse } from '@/lib/mcp/types' import { categorizeError, createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' const logger = createLogger('McpToolDiscoveryAPI') @@ -46,6 +47,12 @@ export const GET = withRouteHandler( ) return createMcpSuccessResponse(responseData) } catch (error) { + if ( + error instanceof McpOauthAuthorizationRequiredError || + error instanceof UnauthorizedError + ) { + return createMcpErrorResponse(error, 'OAuth re-authorization required', 401) + } logger.error(`[${requestId}] Error discovering MCP tools:`, error) const { message, status } = categorizeError(error) return createMcpErrorResponse(new Error(message), 'Failed to discover MCP tools', status) @@ -100,6 +107,12 @@ export const POST = withRouteHandler( }, }) } catch (error) { + if ( + error instanceof McpOauthAuthorizationRequiredError || + error instanceof UnauthorizedError + ) { + return createMcpErrorResponse(error, 'OAuth re-authorization required', 401) + } logger.error(`[${requestId}] Error refreshing tool discovery:`, error) const { message, status } = categorizeError(error) return createMcpErrorResponse(new Error(message), 'Failed to refresh tool discovery', status) diff --git a/apps/sim/app/api/mcp/tools/execute/route.ts b/apps/sim/app/api/mcp/tools/execute/route.ts index d9458deceab..8599a5fcadf 100644 --- a/apps/sim/app/api/mcp/tools/execute/route.ts +++ b/apps/sim/app/api/mcp/tools/execute/route.ts @@ -1,5 +1,7 @@ +import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' import { mcpToolExecutionBodySchema } from '@/lib/api/contracts/mcp' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { getExecutionTimeout } from '@/lib/core/execution-limits' @@ -7,8 +9,14 @@ import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { SIM_VIA_HEADER } from '@/lib/execution/call-chain' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' +import { McpOauthRedirectRequired } from '@/lib/mcp/oauth' import { mcpService } from '@/lib/mcp/service' -import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types' +import { + McpOauthAuthorizationRequiredError, + type McpTool, + type McpToolCall, + type McpToolResult, +} from '@/lib/mcp/types' import { categorizeError, createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils' import { assertPermissionsAllowed, @@ -43,6 +51,7 @@ function hasType(prop: unknown): prop is SchemaProperty { */ export const POST = withRouteHandler( withMcpAuth('read')(async (request: NextRequest, { userId, workspaceId, requestId }) => { + let serverId: string | undefined try { const rawBody = getParsedBody(request) ?? (await request.json()) const parsedBody = mcpToolExecutionBodySchema.safeParse(rawBody) @@ -63,7 +72,8 @@ export const POST = withRouteHandler( userId: userId, }) - const { serverId, toolName, arguments: rawArgs } = body + const { toolName, arguments: rawArgs } = body + serverId = body.serverId const args = rawArgs || {} try { @@ -101,7 +111,8 @@ export const POST = withRouteHandler( if (tool.inputSchema?.properties) { for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) { - const schema = paramSchema as any + const schema = hasType(paramSchema) ? paramSchema : null + if (!schema) continue const value = args[paramName] if (value === undefined || value === null) { @@ -185,12 +196,18 @@ export const POST = withRouteHandler( extraHeaders[SIM_VIA_HEADER] = simViaHeader } + let timeoutHandle: ReturnType | undefined const result = await Promise.race([ mcpService.executeTool(userId, serverId, toolCall, workspaceId, extraHeaders), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout) - ), - ]) + new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error('Tool execution timeout')), + executionTimeout + ) + }), + ]).finally(() => { + if (timeoutHandle !== undefined) clearTimeout(timeoutHandle) + }) const transformedResult = transformToolResult(result) @@ -218,6 +235,27 @@ export const POST = withRouteHandler( return createMcpSuccessResponse(transformedResult) } catch (error) { + if ( + error instanceof McpOauthAuthorizationRequiredError || + error instanceof McpOauthRedirectRequired || + error instanceof UnauthorizedError + ) { + const errorServerId = + error instanceof McpOauthAuthorizationRequiredError ? error.serverId : serverId + logger.warn(`[${requestId}] OAuth re-authorization required for MCP tool execution`, { + serverId: errorServerId, + }) + return NextResponse.json( + { + success: false, + error: 'OAuth re-authorization required', + code: 'reauth_required', + serverId: errorServerId, + }, + { status: 401 } + ) + } + logger.error(`[${requestId}] Error executing MCP tool:`, error) const { message, status } = categorizeError(error) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 4bda12945a5..07a3cf78181 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -32,6 +32,7 @@ import { import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { + DEFAULT_BLOCK_COLOR, formatCostAmount, formatTokenCount, formatTps, @@ -120,6 +121,21 @@ function iconColorClass(bgColor: string): string { return r * 299 + g * 587 + b * 114 > 160_000 ? 'text-[#111111]' : 'text-white' } +/** + * Near-black bgColors disappear against the dark-mode surface (--bg: #1b1b1b). + * Below the luminance threshold we fall back to the neutral block color used + * for blocks with no distinct identity; everything brighter passes through. + */ +function adjustBgForContrast(bgColor: string): string { + const hex = bgColor.replace('#', '') + if (hex.length !== 6) return bgColor + const r = Number.parseInt(hex.slice(0, 2), 16) + const g = Number.parseInt(hex.slice(2, 4), 16) + const b = Number.parseInt(hex.slice(4, 6), 16) + if (r * 299 + g * 587 + b * 114 < 30_000) return DEFAULT_BLOCK_COLOR + return bgColor +} + /** * Flattens the visible (expanded) span tree into a linear list for keyboard * navigation, carrying depth, the chain of parent ids for indent drawing, and @@ -268,7 +284,12 @@ const TraceTreeRow = memo(function TraceTreeRow({ const duration = span.duration || endMs - startMs const isRootWorkflow = depth === 0 && span.type?.toLowerCase() === 'workflow' const hasError = isRootWorkflow ? hasUnhandledErrorInTree(span) : hasErrorInTree(span) - const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name, span.provider) + const { icon: BlockIcon, bgColor: rawBgColor } = getBlockIconAndColor( + span.type, + span.name, + span.provider + ) + const bgColor = adjustBgForContrast(rawBgColor) const nameMatches = !!matchQuery && spanMatchesQuery(span, matchQuery) const offsetMs = runStartMs > 0 ? Math.max(0, startMs - runStartMs) : 0 @@ -651,7 +672,12 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa } const duration = span.duration || parseTime(span.endTime) - parseTime(span.startTime) - const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name, span.provider) + const { icon: BlockIcon, bgColor: rawBgColor } = getBlockIconAndColor( + span.type, + span.name, + span.provider + ) + const bgColor = adjustBgForContrast(rawBgColor) const isRootWorkflow = span.type?.toLowerCase() === 'workflow' const hasError = isRootWorkflow ? hasUnhandledErrorInTree(span) : hasErrorInTree(span) const isDirectError = span.status === 'error' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index afdf52bdaab..4f142b0c67d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -8,6 +8,20 @@ import { getBlock, getBlockByToolName } from '@/blocks' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { normalizeToolId } from '@/tools/normalize' +/** + * Extracts the bare tool name from an MCP tool id of the form + * `mcp-{serverId}-{toolName}`. Returns null when the id is not MCP-shaped. + * Kept local to avoid importing from `@/lib/mcp/utils`, which pulls in + * `next/server` and breaks client bundles. + */ +function tryParseMcpToolName(toolId: string): string | null { + if (!toolId.startsWith('mcp-')) return null + const parts = toolId.split('-') + if (parts.length < 3) return null + const toolName = parts.slice(2).join('-') + return toolName.length > 0 ? toolName : null +} + export const DEFAULT_BLOCK_COLOR = '#6b7280' export interface BlockIconAndColor { @@ -41,6 +55,10 @@ export function getBlockIconAndColor( ): BlockIconAndColor { const lowerType = type.toLowerCase() if (lowerType === 'tool' && toolName) { + if (tryParseMcpToolName(toolName)) { + const mcpBlock = getBlock('mcp') + if (mcpBlock) return { icon: mcpBlock.icon, bgColor: mcpBlock.bgColor } + } const normalized = normalizeToolId(toolName) if (normalized === 'load_skill') return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } const toolBlock = getBlockByToolName(normalized) @@ -90,7 +108,11 @@ export function formatTps( } export function getDisplayName(span: TraceSpan): string { - if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name) + if (span.type?.toLowerCase() === 'tool') { + const mcpToolName = tryParseMcpToolName(span.name) + if (mcpToolName) return mcpToolName + return normalizeToolId(span.name) + } return span.name } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/form-field/form-field.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/form-field/form-field.tsx index 04beeb1484a..cad5381d1d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/form-field/form-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/form-field/form-field.tsx @@ -9,7 +9,7 @@ interface FormFieldProps { export function FormField({ label, children, optional }: FormFieldProps) { return (
-
) : ( -
+
+ + -
- Headers +
{(formData.headers || []).map((header, index) => ( ))}
-
+
+ + + {showAdvanced && ( +
+ + { + if (testResult) clearTestResult() + if (submitError) setSubmitError(null) + setFormData((prev) => ({ ...prev, oauthClientId: e.target.value })) + }} + className='h-9' + /> + + + { + if (testResult) clearTestResult() + if (submitError) setSubmitError(null) + setOauthClientSecretTouched(value.length > 0) + setFormData((prev) => ({ ...prev, oauthClientSecret: value })) + }} + className='h-9' + /> + +

+ Only needed for servers that don't support automatic client registration. +

+
+ )}
)} - + {submitError && ( -

{submitError}

+

{submitError}

)}
@@ -716,7 +812,7 @@ export function McpServerFormModal({ )}
- {formMode === 'json' ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx index a21da0f563e..e8bc927a284 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { ChevronDown, Plus, Search } from 'lucide-react' @@ -27,6 +27,7 @@ import { type McpToolIssue, } from '@/lib/mcp/tool-validation' import type { McpTransport } from '@/lib/mcp/types' +import { useMcpOauthPopup } from '@/hooks/mcp/use-mcp-oauth-popup' import { type McpServer, type McpTool, @@ -102,7 +103,10 @@ function ServerListItem({ ({transportLabel})

{isRefreshing ? 'Refreshing...' @@ -123,14 +127,29 @@ function ServerListItem({ ) } +function buildEditInitialData(server: McpServer) { + const entries: { key: string; value: string }[] = server.headers + ? Object.entries(server.headers).map(([key, value]) => ({ key, value })) + : [] + if (entries.length === 0) entries.push({ key: '', value: '' }) + const last = entries[entries.length - 1] + if (last.key !== '' || last.value !== '') entries.push({ key: '', value: '' }) + + return { + name: server.name || '', + transport: (server.transport as McpTransport) || 'streamable-http', + url: server.url || '', + timeout: 30000, + headers: entries, + oauthClientId: server.oauthClientId || undefined, + hasOauthClientSecret: server.hasOauthClientSecret === true, + } +} + interface MCPProps { initialServerId?: string | null } -/** - * MCP Settings component for managing Model Context Protocol servers. - * Handles server CRUD operations, connection testing, and environment variable integration. - */ export function MCP({ initialServerId }: MCPProps) { const params = useParams() const workspaceId = params.workspaceId as string @@ -147,7 +166,8 @@ export function MCP({ initialServerId }: MCPProps) { isFetching: toolsFetching, } = useMcpToolsQuery(workspaceId) const { data: storedTools = [], refetch: refetchStoredTools } = useStoredMcpTools(workspaceId) - const forceRefreshTools = useForceRefreshMcpTools() + const forceRefreshToolsMutation = useForceRefreshMcpTools() + const forceRefreshTools = forceRefreshToolsMutation.mutate const createServerMutation = useCreateMcpServer() const deleteServerMutation = useDeleteMcpServer() const refreshServerMutation = useRefreshMcpServer() @@ -156,23 +176,16 @@ export function MCP({ initialServerId }: MCPProps) { const { data: allowedMcpDomains = null } = useAllowedMcpDomains() const [showAddModal, setShowAddModal] = useState(false) - const [showEditModal, setShowEditModal] = useState(false) - const [editInitialData, setEditInitialData] = useState< - | { - name: string - transport: McpTransport - url?: string - timeout?: number - headers?: { key: string; value: string }[] - } - | undefined - >(undefined) + const [editingServerId, setEditingServerId] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [deletingServers, setDeletingServers] = useState>(() => new Set()) + const { connectingServers: connectingOauthServers, startOauthForServer } = useMcpOauthPopup({ + workspaceId, + }) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null) + const [serverToDeleteId, setServerToDeleteId] = useState(null) + const showDeleteDialog = serverToDeleteId !== null const [selectedServerId, setSelectedServerId] = useState(initialServerId ?? null) @@ -185,28 +198,23 @@ export function MCP({ initialServerId }: MCPProps) { } }, []) - const [refreshingServers, setRefreshingServers] = useState< - Record - >({}) const [expandedTools, setExpandedTools] = useState>(() => new Set()) - const handleRemoveServer = useCallback((serverId: string, serverName: string) => { - setServerToDelete({ id: serverId, name: serverName }) - setShowDeleteDialog(true) - }, []) + const handleRemoveServer = (serverId: string) => { + setServerToDeleteId(serverId) + } - const confirmDeleteServer = useCallback(async () => { - if (!serverToDelete) return + const confirmDeleteServer = async () => { + if (!serverToDeleteId) return - setShowDeleteDialog(false) - const { id: serverId, name: serverName } = serverToDelete - setServerToDelete(null) + const serverId = serverToDeleteId + setServerToDeleteId(null) setDeletingServers((prev) => new Set(prev).add(serverId)) try { await deleteServerMutation.mutateAsync({ workspaceId, serverId }) - logger.info(`Removed MCP server: ${serverName}`) + logger.info(`Removed MCP server: ${serverId}`) } catch (error) { logger.error('Failed to remove MCP server:', error) } finally { @@ -216,43 +224,36 @@ export function MCP({ initialServerId }: MCPProps) { return newSet }) } - }, [serverToDelete, deleteServerMutation, workspaceId]) - - const toolsByServer = useMemo(() => { - return (mcpToolsData || []).reduce( - (acc, tool) => { - if (!tool?.serverId) return acc - if (!acc[tool.serverId]) { - acc[tool.serverId] = [] - } - acc[tool.serverId].push(tool) - return acc - }, - {} as Record - ) - }, [mcpToolsData]) - - const filteredServers = useMemo(() => { - return (servers || []).filter((server) => - server.name?.toLowerCase().includes(searchTerm.toLowerCase()) - ) - }, [servers, searchTerm]) + } - const handleViewDetails = useCallback( - (serverId: string) => { - setSelectedServerId(serverId) - forceRefreshTools(workspaceId) - refetchStoredTools() + const toolsByServer = (mcpToolsData || []).reduce( + (acc, tool) => { + if (!tool?.serverId) return acc + if (!acc[tool.serverId]) { + acc[tool.serverId] = [] + } + acc[tool.serverId].push(tool) + return acc }, - [workspaceId, forceRefreshTools, refetchStoredTools] + {} as Record + ) + + const filteredServers = (servers || []).filter((server) => + server.name?.toLowerCase().includes(searchTerm.toLowerCase()) ) - const handleBackToList = useCallback(() => { + const handleViewDetails = (serverId: string) => { + setSelectedServerId(serverId) + forceRefreshTools(workspaceId) + refetchStoredTools() + } + + const handleBackToList = () => { setSelectedServerId(null) setExpandedTools(new Set()) - }, []) + } - const toggleToolExpanded = useCallback((toolName: string) => { + const toggleToolExpanded = (toolName: string) => { setExpandedTools((prev) => { const newSet = new Set(prev) if (newSet.has(toolName)) { @@ -262,131 +263,109 @@ export function MCP({ initialServerId }: MCPProps) { } return newSet }) - }, []) + } - const handleRefreshServer = useCallback( - async (serverId: string) => { - try { - setRefreshingServers((prev) => ({ ...prev, [serverId]: { status: 'refreshing' } })) - const result = await refreshServerMutation.mutateAsync({ workspaceId, serverId }) - logger.info( - `Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}` - ) - - const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId - if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) { - logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`) - try { - const { data: workflowData } = await requestJson(getWorkflowStateContract, { - params: { id: activeWorkflowId }, - }) - if (workflowData?.state?.blocks) { - useSubBlockStore - .getState() - .initializeFromWorkflow( - activeWorkflowId, - workflowData.state.blocks as Record - ) - } - } catch (reloadError) { - logger.warn('Failed to reload workflow subblock values:', reloadError) - } - } + const handleRefreshServer = async (serverId: string) => { + try { + const result = await refreshServerMutation.mutateAsync({ workspaceId, serverId }) + logger.info( + `Refreshed MCP server: ${serverId}, workflows updated: ${result.workflowsUpdated}` + ) - setRefreshingServers((prev) => ({ - ...prev, - [serverId]: { status: 'refreshed', workflowsUpdated: result.workflowsUpdated }, - })) - setTimeout(() => { - setRefreshingServers((prev) => { - const newState = { ...prev } - delete newState[serverId] - return newState + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + if (activeWorkflowId && result.updatedWorkflowIds?.includes(activeWorkflowId)) { + logger.info(`Active workflow ${activeWorkflowId} was updated, reloading subblock values`) + try { + const { data: workflowData } = await requestJson(getWorkflowStateContract, { + params: { id: activeWorkflowId }, }) - }, 3000) - } catch (error) { - logger.error('Failed to refresh MCP server:', error) - setRefreshingServers((prev) => { - const newState = { ...prev } - delete newState[serverId] - return newState - }) + if (workflowData?.state?.blocks) { + useSubBlockStore + .getState() + .initializeFromWorkflow( + activeWorkflowId, + workflowData.state.blocks as Record + ) + } + } catch (reloadError) { + logger.warn('Failed to reload workflow subblock values:', reloadError) + } } - }, - [refreshServerMutation, workspaceId] - ) - - const handleOpenEditModal = useCallback((server: McpServer) => { - const headers: { key: string; value: string }[] = server.headers - ? Object.entries(server.headers).map(([key, value]) => ({ key, value })) - : [{ key: '', value: '' }] - if (headers.length === 0) headers.push({ key: '', value: '' }) - - const lastHeader = headers[headers.length - 1] - if (lastHeader.key !== '' || lastHeader.value !== '') { - headers.push({ key: '', value: '' }) + } catch (error) { + logger.error('Failed to refresh MCP server:', error) } + } - setEditInitialData({ - name: server.name || '', - transport: (server.transport as McpTransport) || 'streamable-http', - url: server.url || '', - timeout: 30000, - headers, - }) - setShowEditModal(true) - }, []) - - const selectedServer = useMemo(() => { + useEffect(() => { + if (!refreshServerMutation.isSuccess) return + const timeout = window.setTimeout(() => refreshServerMutation.reset(), 3000) + return () => window.clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps -- mutation object is unstable; isSuccess flag is the trigger + }, [refreshServerMutation.isSuccess]) + + const refreshingServerId = refreshServerMutation.isPending + ? refreshServerMutation.variables?.serverId + : null + const refreshedServerId = refreshServerMutation.isSuccess + ? refreshServerMutation.variables?.serverId + : null + const refreshedWorkflowsUpdated = refreshServerMutation.data?.workflowsUpdated + + const editingServer = editingServerId + ? (servers.find((s) => s.id === editingServerId) as McpServer | undefined) + : undefined + const editInitialData = editingServer ? buildEditInitialData(editingServer) : undefined + + const selectedServer = (() => { if (!selectedServerId) return null const server = servers.find((s) => s.id === selectedServerId) as McpServer | undefined if (!server) return null const serverTools = (toolsByServer[selectedServerId] || []) as McpTool[] return { server, tools: serverTools } - }, [selectedServerId, servers, toolsByServer]) + })() + + const getStoredToolIssues = ( + serverId: string, + toolName: string + ): { issue: McpToolIssue; workflowName: string }[] => { + const relevantStoredTools = storedTools.filter( + (st) => st.serverId === serverId && st.toolName === toolName + ) - const getStoredToolIssues = useCallback( - (serverId: string, toolName: string): { issue: McpToolIssue; workflowName: string }[] => { - const relevantStoredTools = storedTools.filter( - (st) => st.serverId === serverId && st.toolName === toolName + const serverStates = servers.map((s) => ({ + id: s.id, + url: s.url, + connectionStatus: s.connectionStatus, + lastError: s.lastError || undefined, + })) + + const discoveredTools = mcpToolsData.map((t) => ({ + serverId: t.serverId, + name: t.name, + inputSchema: t.inputSchema, + })) + + const issues: { issue: McpToolIssue; workflowName: string }[] = [] + + for (const storedTool of relevantStoredTools) { + const issue = getMcpToolIssue( + { + serverId: storedTool.serverId, + serverUrl: storedTool.serverUrl, + toolName: storedTool.toolName, + schema: storedTool.schema, + }, + serverStates, + discoveredTools ) - const serverStates = servers.map((s) => ({ - id: s.id, - url: s.url, - connectionStatus: s.connectionStatus, - lastError: s.lastError || undefined, - })) - - const discoveredTools = mcpToolsData.map((t) => ({ - serverId: t.serverId, - name: t.name, - inputSchema: t.inputSchema, - })) - - const issues: { issue: McpToolIssue; workflowName: string }[] = [] - - for (const storedTool of relevantStoredTools) { - const issue = getMcpToolIssue( - { - serverId: storedTool.serverId, - serverUrl: storedTool.serverUrl, - toolName: storedTool.toolName, - schema: storedTool.schema, - }, - serverStates, - discoveredTools - ) - - if (issue) { - issues.push({ issue, workflowName: storedTool.workflowName }) - } + if (issue) { + issues.push({ issue, workflowName: storedTool.workflowName }) } + } - return issues - }, - [storedTools, servers, mcpToolsData] - ) + return issues + } const error = toolsError || serversError const hasServers = servers && servers.length > 0 @@ -422,12 +401,32 @@ export function MCP({ initialServerId }: MCPProps) { {server.connectionStatus === 'error' && (

Status -

+

{server.lastError || 'Unable to connect'}

)} + {server.authType === 'oauth' && server.connectionStatus !== 'connected' && ( +
+ + Authentication + +
+ +
+
+ )} +
Tools ({tools.length}) @@ -450,11 +449,12 @@ export function MCP({ initialServerId }: MCPProps) { key={tool.name} className='overflow-hidden rounded-md border bg-[var(--surface-3)]' > - + {isExpanded && hasParams && (
@@ -563,25 +563,27 @@ export function MCP({ initialServerId }: MCPProps) { -
{ + if (!open) setEditingServerId(null) + }} mode='edit' initialData={editInitialData} onSubmit={async (config) => { @@ -620,7 +622,7 @@ export function MCP({ initialServerId }: MCPProps) { />
@@ -628,7 +630,7 @@ export function MCP({ initialServerId }: MCPProps) {
{error ? (
-

+

{getErrorMessage(error, 'Failed to load MCP servers')}

@@ -656,8 +658,8 @@ export function MCP({ initialServerId }: MCPProps) { tools={tools} isDeleting={deletingServers.has(server.id)} isLoadingTools={isLoadingTools} - isRefreshing={refreshingServers[server.id]?.status === 'refreshing'} - onRemove={() => handleRemoveServer(server.id, server.name || 'this server')} + isRefreshing={refreshingServerId === server.id} + onRemove={() => handleRemoveServer(server.id)} onViewDetails={() => handleViewDetails(server.id)} /> ) @@ -677,28 +679,38 @@ export function MCP({ initialServerId }: MCPProps) { onOpenChange={setShowAddModal} mode='add' onSubmit={async (config) => { - await createServerMutation.mutateAsync({ + const result = await createServerMutation.mutateAsync({ workspaceId, config: { ...config, enabled: true }, }) + if (result.authType === 'oauth') { + await startOauthForServer(result.serverId) + } }} workspaceId={workspaceId} availableEnvVars={availableEnvVars} allowedMcpDomains={allowedMcpDomains} /> - + { + if (!open) setServerToDeleteId(null) + }} + > Delete MCP Server Are you sure you want to delete{' '} - {serverToDelete?.name} + + {servers.find((s) => s.id === serverToDeleteId)?.name || 'this server'} + ? This action cannot be undone. -