From dd984b484846ab7323c476dff1570cedb68eef56 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 24 Jul 2026 21:45:39 -0700 Subject: [PATCH 01/33] feat(tables): saved views with filter, sort, and column presets --- .../table/[tableId]/views/[viewId]/route.ts | 95 + .../app/api/table/[tableId]/views/route.ts | 96 + .../resource-options/resource-options.tsx | 15 +- .../components/columns-menu/columns-menu.tsx | 185 + .../components/columns-menu/index.ts | 1 + .../tables/[tableId]/components/index.ts | 3 + .../components/save-view-modal/index.ts | 1 + .../save-view-modal/save-view-modal.tsx | 76 + .../components/table-grid/table-grid.tsx | 18 +- .../[tableId]/components/views-menu/index.ts | 1 + .../components/views-menu/views-menu.tsx | 237 + .../[workspaceId]/tables/[tableId]/page.tsx | 16 +- .../tables/[tableId]/search-params.ts | 20 +- .../[workspaceId]/tables/[tableId]/table.tsx | 278 +- apps/sim/hooks/queries/tables.ts | 107 + apps/sim/hooks/queries/utils/table-keys.ts | 8 + apps/sim/lib/api/contracts/tables.ts | 113 + apps/sim/lib/core/config/env.ts | 1 + .../sim/lib/core/config/feature-flags.test.ts | 24 + apps/sim/lib/core/config/feature-flags.ts | 11 + apps/sim/lib/table/index.ts | 1 + apps/sim/lib/table/types.ts | 24 +- apps/sim/lib/table/views/service.test.ts | 52 + apps/sim/lib/table/views/service.ts | 200 + packages/db/migrations/0270_table_views.sql | 17 + .../db/migrations/meta/0270_snapshot.json | 17829 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 48 + scripts/check-api-validation-contracts.ts | 4 +- 29 files changed, 19462 insertions(+), 26 deletions(-) create mode 100644 apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts create mode 100644 apps/sim/app/api/table/[tableId]/views/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx create mode 100644 apps/sim/lib/table/views/service.test.ts create mode 100644 apps/sim/lib/table/views/service.ts create mode 100644 packages/db/migrations/0270_table_views.sql create mode 100644 packages/db/migrations/meta/0270_snapshot.json diff --git a/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts new file mode 100644 index 00000000000..3bfefb7fa66 --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts @@ -0,0 +1,95 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { deleteTableViewContract, updateTableViewContract } from '@/lib/api/contracts/tables' +import { parseRequest, 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 { TableSchema } from '@/lib/table' +import { deleteTableView, TableViewValidationError, updateTableView } from '@/lib/table' +import { accessError, checkAccess } from '@/app/api/table/utils' + +const logger = createLogger('TableViewAPI') + +interface TableViewRouteParams { + params: Promise<{ tableId: string; viewId: string }> +} + +/** PATCH /api/table/[tableId]/views/[viewId] - Rename, overwrite config, or set as default. */ +export const PATCH = withRouteHandler( + async (request: NextRequest, context: TableViewRouteParams) => { + 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(updateTableViewContract, request, context, { + validationErrorResponse: (error) => validationErrorResponse(error), + }) + if (!parsed.success) return parsed.response + + const { tableId, viewId } = parsed.data.params + const { workspaceId, name, config, isDefault } = parsed.data.body + + const result = await checkAccess(tableId, authResult.userId, 'write') + if (!result.ok) return accessError(result, requestId, tableId) + + if (result.table.workspaceId !== workspaceId) { + return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) + } + + const columns = (result.table.schema as TableSchema).columns ?? [] + const view = await updateTableView({ viewId, tableId, name, config, isDefault, columns }) + + return NextResponse.json({ success: true, data: { view } }) + } catch (error) { + if (error instanceof TableViewValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + logger.error(`[${requestId}] Error updating table view:`, error) + return NextResponse.json({ error: 'Failed to update view' }, { status: 500 }) + } + } +) + +/** DELETE /api/table/[tableId]/views/[viewId] - Remove a saved view. */ +export const DELETE = withRouteHandler( + async (request: NextRequest, context: TableViewRouteParams) => { + 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(deleteTableViewContract, request, context, { + validationErrorResponse: (error) => validationErrorResponse(error), + }) + if (!parsed.success) return parsed.response + + const { tableId, viewId } = parsed.data.params + const { workspaceId } = parsed.data.body + + const result = await checkAccess(tableId, authResult.userId, 'write') + if (!result.ok) return accessError(result, requestId, tableId) + + if (result.table.workspaceId !== workspaceId) { + return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) + } + + const deleted = await deleteTableView(viewId, tableId) + if (!deleted) { + return NextResponse.json({ error: 'View not found' }, { status: 404 }) + } + + return NextResponse.json({ success: true, data: { deleted: true } }) + } catch (error) { + logger.error(`[${requestId}] Error deleting table view:`, error) + return NextResponse.json({ error: 'Failed to delete view' }, { status: 500 }) + } + } +) diff --git a/apps/sim/app/api/table/[tableId]/views/route.ts b/apps/sim/app/api/table/[tableId]/views/route.ts new file mode 100644 index 00000000000..975267d8a84 --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/views/route.ts @@ -0,0 +1,96 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { createTableViewContract, listTableViewsContract } from '@/lib/api/contracts/tables' +import { parseRequest, 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 { TableSchema } from '@/lib/table' +import { createTableView, listTableViews, TableViewValidationError } from '@/lib/table' +import { accessError, checkAccess } from '@/app/api/table/utils' + +const logger = createLogger('TableViewsAPI') + +interface TableRouteParams { + params: Promise<{ tableId: string }> +} + +/** GET /api/table/[tableId]/views - List every saved view on a table. */ +export const GET = withRouteHandler(async (request: NextRequest, context: TableRouteParams) => { + 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(listTableViewsContract, request, context, { + validationErrorResponse: (error) => validationErrorResponse(error), + }) + if (!parsed.success) return parsed.response + + const { tableId } = parsed.data.params + const { workspaceId } = parsed.data.query + + const result = await checkAccess(tableId, authResult.userId, 'read') + if (!result.ok) return accessError(result, requestId, tableId) + + if (result.table.workspaceId !== workspaceId) { + return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) + } + + const columns = (result.table.schema as TableSchema).columns ?? [] + const views = await listTableViews(tableId, columns) + + return NextResponse.json({ success: true, data: { views } }) + } catch (error) { + logger.error(`[${requestId}] Error listing table views:`, error) + return NextResponse.json({ error: 'Failed to list views' }, { status: 500 }) + } +}) + +/** POST /api/table/[tableId]/views - Save the current filter/sort/layout as a named view. */ +export const POST = withRouteHandler(async (request: NextRequest, context: TableRouteParams) => { + 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(createTableViewContract, request, context, { + validationErrorResponse: (error) => validationErrorResponse(error), + }) + if (!parsed.success) return parsed.response + + const { tableId } = parsed.data.params + const { workspaceId, name, config } = parsed.data.body + + const result = await checkAccess(tableId, authResult.userId, 'write') + if (!result.ok) return accessError(result, requestId, tableId) + + if (result.table.workspaceId !== workspaceId) { + return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) + } + + const columns = (result.table.schema as TableSchema).columns ?? [] + const view = await createTableView({ + tableId, + workspaceId, + name, + config, + userId: authResult.userId, + columns, + }) + + return NextResponse.json({ success: true, data: { view } }) + } catch (error) { + if (error instanceof TableViewValidationError) { + return NextResponse.json({ error: error.message }, { status: 400 }) + } + logger.error(`[${requestId}] Error creating table view:`, error) + return NextResponse.json({ error: 'Failed to create view' }, { status: 500 }) + } +}) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx index af98a4ccfbf..ebbf38ed20f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx @@ -91,6 +91,12 @@ interface ResourceOptionsProps { * widgets; primary actions belong in the header's `actions`. */ aside?: ReactNode + /** + * Control pinned to the far RIGHT of the bar, opposite the filter/sort cluster — + * e.g. the table editor's Save-view button. Unlike `aside` it is a real action, + * so it is separated from the menu group rather than joined to it. + */ + trailing?: ReactNode } export const ResourceOptions = memo(function ResourceOptions({ @@ -99,6 +105,7 @@ export const ResourceOptions = memo(function ResourceOptions({ filter, filterTags, aside, + trailing, }: ResourceOptionsProps) { /** * Coordinates the Filter popover and Sort menu as a single menu bar: clicking @@ -111,14 +118,17 @@ export const ResourceOptions = memo(function ResourceOptions({ const isToggleFilter = filter?.mode === 'toggle' const popoverFilter = filter && filter.mode !== 'toggle' ? filter : null - const hasContent = search || sort || filter || aside || (filterTags && filterTags.length > 0) + const hasContent = + search || sort || filter || aside || trailing || (filterTags && filterTags.length > 0) if (!hasContent) return null return (
{search && } -
+ {/* `ml-auto` moves to `trailing` when present so the menu cluster stays put + and only the trailing action is pushed to the far edge. */} +
{aside}
{filterTags?.map((tag) => ( @@ -178,6 +188,7 @@ export const ResourceOptions = memo(function ResourceOptions({ {sort && (isToggleFilter || !popoverFilter) && }
+ {trailing &&
{trailing}
}
) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx new file mode 100644 index 00000000000..d7f0d57eddc --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx @@ -0,0 +1,185 @@ +'use client' + +import { memo, useMemo, useState } from 'react' +import { + Chip, + cn, + POPOVER_ANIMATION_CLASSES, + Popover, + PopoverContent, + PopoverItem, + PopoverSection, + PopoverTrigger, +} from '@sim/emcn' +import { Columns3, Eye, EyeOff } from '@sim/emcn/icons' +import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' +import { getColumnId } from '@/lib/table/column-keys' + +interface ColumnsMenuProps { + columns: ColumnDefinition[] + workflowGroups: WorkflowGroup[] + /** **Column ids** currently hidden from the grid. */ + hiddenColumns: string[] + onChange: (hiddenColumns: string[]) => void +} + +/** + * Show/hide picker for grid columns. Workflow output columns nest under their + * group with a parent toggle that flips all of its children at once — the + * group's spanning header is derived from whichever children stay visible, so + * hiding them all removes the header too. + */ +export const ColumnsMenu = memo(function ColumnsMenu({ + columns, + workflowGroups, + hiddenColumns, + onChange, +}: ColumnsMenuProps) { + const [open, setOpen] = useState(false) + const hiddenSet = new Set(hiddenColumns) + + const { plain, groups } = useMemo(() => { + const plainColumns: ColumnDefinition[] = [] + const byGroup = new Map() + for (const col of columns) { + if (col.workflowGroupId) { + const existing = byGroup.get(col.workflowGroupId) + if (existing) existing.push(col) + else byGroup.set(col.workflowGroupId, [col]) + } else { + plainColumns.push(col) + } + } + const groupById = new Map(workflowGroups.map((group) => [group.id, group])) + return { + plain: plainColumns, + groups: [...byGroup.entries()].map(([groupId, children]) => ({ + groupId, + name: groupById.get(groupId)?.name ?? 'Workflow', + children, + })), + } + }, [columns, workflowGroups]) + + /** + * Flips one or more columns. Rejects a change that would hide every column — + * an empty grid has nothing left to act on. + */ + const toggle = (ids: string[], nextHidden: boolean) => { + const next = new Set(hiddenColumns) + for (const id of ids) { + if (nextHidden) next.add(id) + else next.delete(id) + } + if (next.size >= columns.length) return + onChange([...next]) + } + + const hiddenCount = hiddenColumns.length + + return ( + + + 0} leftIcon={Columns3}> + {hiddenCount > 0 ? `Columns (${hiddenCount} hidden)` : 'Columns'} + + + + + Columns + +
+ {plain.map((col) => { + const id = getColumnId(col) + return ( + toggle([id], !nextVisible)} + /> + ) + })} + {groups.map((group) => { + const childIds = group.children.map(getColumnId) + const visibleCount = childIds.filter((id) => !hiddenSet.has(id)).length + return ( +
+ {/* `visible` is all-children-visible, so a partial group reads as + not-yet-visible and one click reveals the rest rather than + hiding what is still showing. */} + 0 && visibleCount < childIds.length} + onToggle={(nextVisible) => toggle(childIds, !nextVisible)} + /> + {group.children.map((col) => { + const id = getColumnId(col) + return ( + toggle([id], !nextVisible)} + /> + ) + })} +
+ ) + })} +
+
+
+ ) +}) + +interface ColumnToggleRowProps { + label: string + visible: boolean + /** Group parent whose children are only partly visible. */ + partial?: boolean + indented?: boolean + onToggle: (nextVisible: boolean) => void +} + +function ColumnToggleRow({ label, visible, partial, indented, onToggle }: ColumnToggleRowProps) { + // A partial group still has something on screen, so it keeps the "shown" icon + // at reduced strength rather than flipping to the hidden one. + const showing = visible || partial + const Icon = showing ? Eye : EyeOff + return ( + onToggle(!visible)} + className={cn('h-7 items-center gap-1.5 px-1.5 py-0 text-xs', indented && 'pl-5')} + > + + + + + {label} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/index.ts new file mode 100644 index 00000000000..82894e8de55 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/index.ts @@ -0,0 +1 @@ +export { ColumnsMenu } from './columns-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts index d458df4c60a..1b34f11084d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts @@ -1,12 +1,15 @@ export * from './column-config-sidebar' +export * from './columns-menu' export * from './context-menu' export * from './enrichment-details' export * from './enrichments-sidebar' export * from './new-column-dropdown' export * from './row-modal' export * from './run-status-control' +export * from './save-view-modal' export * from './sidebar-fields' export * from './table-action-bar' export * from './table-filter' export * from './table-grid' +export * from './views-menu' export * from './workflow-sidebar' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/index.ts new file mode 100644 index 00000000000..691ca12aa65 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/index.ts @@ -0,0 +1 @@ +export { SaveViewModal } from './save-view-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx new file mode 100644 index 00000000000..b8f1dfcc742 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' +import { + ChipModal, + ChipModalBody, + ChipModalField, + ChipModalFooter, + ChipModalHeader, +} from '@sim/emcn' + +interface SaveViewModalProps { + open: boolean + onOpenChange: (open: boolean) => void + /** Pre-filled when renaming an existing view; empty when saving a new one. */ + initialName?: string + mode: 'create' | 'rename' + onSubmit: (name: string) => void + isSubmitting: boolean +} + +/** + * Names a view — used both for "Save as view" and for renaming an existing one. + * A view name is free-form (no identifier rules), so the only guard is emptiness. + */ +export function SaveViewModal({ + open, + onOpenChange, + initialName = '', + mode, + onSubmit, + isSubmitting, +}: SaveViewModalProps) { + const [name, setName] = useState(initialName) + // Seed the field on each open rather than syncing in an effect: the modal stays + // mounted between opens, so without this a second open shows the stale name. + const [prevOpen, setPrevOpen] = useState(open) + if (prevOpen !== open) { + setPrevOpen(open) + if (open) setName(initialName) + } + + const trimmed = name.trim() + const title = mode === 'create' ? 'Save as view' : 'Rename view' + + const handleSubmit = () => { + if (!trimmed || isSubmitting) return + onSubmit(trimmed) + } + + return ( + + onOpenChange(false)}>{title} + + {/* Enter fires the footer's primary action automatically — no key wiring. */} + + + onOpenChange(false)} + cancelDisabled={isSubmitting} + primaryAction={{ + label: isSubmitting ? 'Saving...' : 'Save', + onClick: handleSubmit, + disabled: !trimmed || isSubmitting, + }} + /> + + ) +} 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 330df1c2d2d..51c8dd49a97 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 @@ -209,6 +209,11 @@ interface TableGridProps { onSelectionChange: (state: SelectionSnapshot) => void /** Filter + sort. Lifted to wrapper so a single `useTable` call serves both. */ queryOptions: QueryOptions + /** + * **Column ids** to hide from the grid. Owned by the wrapper because the filter + * panel's Columns section edits the same list and the active view persists it. + */ + hiddenColumns?: string[] /** * Ref the grid populates with its `handleColumnRename` so the wrapper's * sidebars can fire a column rename back into the grid (rewrites local @@ -307,6 +312,7 @@ export function TableGrid({ onStopRow, onSelectionChange, queryOptions, + hiddenColumns, columnRenameSinkRef, afterDeleteRowsSinkRef, afterDeleteAllSinkRef, @@ -642,8 +648,18 @@ export function TableGrid({ ordered.push(col) } } + // Hidden columns are dropped BEFORE expansion so a workflow group's + // `groupSize` / `isGroupStart` are computed over the surviving children — + // expanding first would leave the spanning header claiming columns that no + // longer render. Hiding every child of a group therefore removes its header + // entirely, and hiding a plain column between two children of the same group + // merges what were two header spans back into one. + if (hiddenColumns && hiddenColumns.length > 0) { + const hidden = new Set(hiddenColumns) + ordered = ordered.filter((col) => !hidden.has(getColumnId(col))) + } return expandToDisplayColumns(ordered, tableWorkflowGroups) - }, [columns, columnOrder, tableWorkflowGroups]) + }, [columns, columnOrder, hiddenColumns, tableWorkflowGroups]) const workflowGroupById = useMemo( () => new Map(tableWorkflowGroups.map((g) => [g.id, g])), diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/index.ts new file mode 100644 index 00000000000..e8835bebdc4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/index.ts @@ -0,0 +1 @@ +export { ALL_ROWS_VIEW_LABEL, ViewsMenu } from './views-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx new file mode 100644 index 00000000000..dc3cd443aea --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx @@ -0,0 +1,237 @@ +'use client' + +import { memo, useEffect, useRef, useState } from 'react' +import { + ChipChevronDown, + chipContentLabelClass, + chipVariants, + cn, + POPOVER_ANIMATION_CLASSES, + Popover, + PopoverAnchor, + PopoverContent, + PopoverItem, + PopoverSection, +} from '@sim/emcn' +import { Check, Pencil, Trash } from '@sim/emcn/icons' +import type { TableViewWire } from '@/lib/api/contracts/tables' + +/** Label for the built-in unfiltered state. Not a stored row — `null` view id. */ +export const ALL_ROWS_VIEW_LABEL = 'All' + +/** Matches the breadcrumb location popover's hover-intent grace period. */ +const POPOVER_CLOSE_DELAY_MS = 120 + +/** Rendered width of one action button (`p-1` + `size-3` glyph) plus its `gap-0.5`. + * The row reserves `actions.length` of these, so keep it in step with the button + * classes below — the overlay is absolutely positioned and can't size the spacer. */ +const VIEW_ACTION_SLOT_PX = 22 + +interface ViewsMenuProps { + views: TableViewWire[] + /** `null` selects the built-in "All" state. */ + activeViewId: string | null + onSelect: (viewId: string | null) => void + onRename: (viewId: string) => void + onDelete: (viewId: string) => void + /** Read-only members can switch views but not modify them. */ + canEdit: boolean +} + +/** + * View switcher for the table options bar. Reads "View" until one is selected, + * then carries the active view's name. + * + * Opens on hover-intent like the header's breadcrumb location popover, so the + * list of views is discoverable without a click. + */ +export const ViewsMenu = memo(function ViewsMenu({ + views, + activeViewId, + onSelect, + onRename, + onDelete, + canEdit, +}: ViewsMenuProps) { + const [open, setOpen] = useState(false) + const closeTimeoutRef = useRef | null>(null) + + const activeView = activeViewId ? views.find((view) => view.id === activeViewId) : undefined + const label = activeView?.name ?? 'View' + + const cancelScheduledClose = () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current) + closeTimeoutRef.current = null + } + } + + const openPopover = () => { + cancelScheduledClose() + setOpen(true) + } + + const scheduleClose = () => { + cancelScheduledClose() + closeTimeoutRef.current = setTimeout(() => { + setOpen(false) + closeTimeoutRef.current = null + }, POPOVER_CLOSE_DELAY_MS) + } + + /** Closes up front so the popover plays its exit animation instead of snapping + * away when a selection re-renders the bar. */ + const runAndClose = (action: () => void) => { + cancelScheduledClose() + setOpen(false) + action() + } + + useEffect(() => { + return () => { + if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current) + } + }, []) + + return ( + + + + + + + Views + +
+ runAndClose(() => onSelect(null))} + /> + {views.map((view) => ( + runAndClose(() => onSelect(view.id))} + actions={ + canEdit + ? [ + { + icon: Pencil, + label: 'Rename', + onClick: () => runAndClose(() => onRename(view.id)), + }, + { + icon: Trash, + label: 'Delete', + onClick: () => runAndClose(() => onDelete(view.id)), + }, + ] + : undefined + } + /> + ))} +
+
+
+ ) +}) + +interface ViewRowAction { + icon: React.ElementType + label: string + onClick: () => void +} + +interface ViewRowProps { + label: string + isActive: boolean + isDefault?: boolean + onSelect: () => void + actions?: ViewRowAction[] +} + +/** + * One row: a full-width select target with the per-view actions laid out beside + * the label rather than over it. The actions occupy real layout width (revealed + * on hover via opacity, not `display`) so the name never reflows or sits + * underneath them. + */ +function ViewRow({ label, isActive, isDefault, onSelect, actions }: ViewRowProps) { + return ( +
+ + + {isActive && } + + {label} + {isDefault && ( + + Default + + )} + {actions && ( + + )} + + {actions && ( +
+ {actions.map((action) => ( + + ))} +
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/page.tsx index 9f3c382c3ff..6a2a2b56eea 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/page.tsx @@ -1,5 +1,8 @@ import { Suspense } from 'react' import type { Metadata } from 'next' +import { getSession } from '@/lib/auth' +import { getActiveOrganizationId } from '@/lib/auth/session-response' +import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import TableLoading from '@/app/workspace/[workspaceId]/tables/[tableId]/loading' import { Table } from './table' @@ -11,11 +14,20 @@ export const metadata: Metadata = { * Table-detail page entry. `Table` reads URL query params via nuqs (which uses * `useSearchParams` internally), so it must sit under a Suspense boundary. The * fallback renders the real chrome so a suspend never shows a blank frame. + * + * The `table-views` flag is resolved here rather than inside `Table` — there is + * no client-side AppConfig, so the boolean is passed down as a prop. */ -export default function TablePage() { +export default async function TablePage() { + const session = await getSession() + const viewsEnabled = await isFeatureEnabled('table-views', { + userId: session?.user?.id, + orgId: getActiveOrganizationId(session) ?? undefined, + }) + return ( }> - +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts index cb2b2914ea9..1b6b9b97643 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts @@ -21,10 +21,28 @@ export const DEFAULT_TABLE_DETAIL_SORT_DIRECTION = 'asc' export const tableDetailParsers = { sort: parseAsString, dir: parseAsStringLiteral(SORT_DIRECTIONS).withDefault(DEFAULT_TABLE_DETAIL_SORT_DIRECTION), + /** + * Active saved view id. Nullable with no default: `null` is the built-in "All" + * state (no view), which is behaviourally distinct from any saved view and is + * what a table with zero views always shows. + * + * Only the id lives here — the view's filter/sort/layout are looked up from the + * loaded list, per the store-the-id-derive-the-object convention. A table's + * default view is resolved on mount and written back explicitly, so a shared + * link keeps pointing at the same view even if someone changes the default. + */ + view: parseAsString, } as const -/** Sort view-state: clean URLs, no back-stack churn. */ +/** + * Sort + view state: clean URLs, no back-stack churn. + * + * The wire key is `table-view`, not `view` — these parsers also bind to the home + * surface via the embedded mothership table, where a bare `view` would collide + * with any future view-mode param there. + */ export const tableDetailUrlKeys = { history: 'replace', clearOnDefault: true, + urlKeys: { view: 'table-view' }, } as const diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 0f7485636bb..976b427f643 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -1,19 +1,22 @@ 'use client' -import { useCallback, useMemo, useReducer, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { Chip, ChipConfirmModal, toast } from '@sim/emcn' import { Download, Pencil, Table as TableIcon, Trash, Upload } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { useParams, useRouter } from 'next/navigation' import { useQueryStates } from 'nuqs' import { usePostHog } from 'posthog-js/react' -import type { RunLimit, RunMode } from '@/lib/api/contracts/tables' +import type { RunLimit, RunMode, TableViewWire } from '@/lib/api/contracts/tables' import { captureEvent } from '@/lib/posthog/client' import type { ColumnDefinition, Filter, Sort, + SortDirection, TableRow as TableRowType, + TableViewConfig, WorkflowGroup, } from '@/lib/table' import { getColumnId } from '@/lib/table/column-keys' @@ -32,11 +35,15 @@ import { useLogByExecutionId } from '@/hooks/queries/logs' import { downloadTableExport, useCancelTableRuns, + useCreateTableView, useDeleteTable, useDeleteTableRowsAsync, + useDeleteTableView, useExportTableAsync, useRenameTable, useRunColumn, + useTableViews, + useUpdateTableView, } from '@/hooks/queries/tables' import { useInlineRename } from '@/hooks/use-inline-rename' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' @@ -45,15 +52,18 @@ import type { DeletedRowSnapshot } from '@/stores/table/types' import { type ColumnConfig, ColumnConfigSidebar, + ColumnsMenu, EnrichmentDetails, EnrichmentsSidebar, NewColumnDropdown, RowModal, RunStatusControl, + SaveViewModal, type SelectionSnapshot, TableActionBar, TableFilter, TableGrid, + ViewsMenu, type WorkflowConfig, WorkflowSidebar, } from './components' @@ -77,6 +87,12 @@ interface TableProps { /** Identifiers — only set in embedded mode. Page mode reads from `useParams()`. */ workspaceId?: string tableId?: string + /** + * Resolved `table-views` flag. Server-only to resolve (no client AppConfig), so + * the page passes it down. Defaults to `false` so the embedded mothership table + * — which has no server context to resolve it — stays on today's Filter/Sort bar. + */ + viewsEnabled?: boolean } /** @@ -118,6 +134,30 @@ function slideoutReducer(_state: SlideoutState, action: SlideoutAction): Slideou } } +/** Stable identity so a loading/disabled views query doesn't remint `[]` each render. */ +const NO_VIEWS: TableViewWire[] = [] + +type ViewModalState = { mode: 'create' } | { mode: 'rename'; viewId: string } | null + +/** + * Structural equality for the parts of a view config the user edits directly. + * Column layout (widths/order/pinning) is excluded — it auto-saves into the + * active view as the user drags, so it can never be the thing that is "unsaved". + * + * Compares JSON rather than field-by-field because `filter` is an arbitrarily + * nested predicate tree; key order is stable since both sides are built by the + * same converters. + */ +function isSameViewConfig(a: TableViewConfig, b: TableViewConfig): boolean { + const normalize = (config: TableViewConfig) => + JSON.stringify({ + filter: config.filter ?? null, + sort: config.sort ?? null, + hiddenColumns: [...(config.hiddenColumns ?? [])].sort(), + }) + return normalize(a) === normalize(b) +} + /** * Page-level wrapper for the table detail view. Mirrors the shape of * `logs/logs.tsx`: a thin orchestrator that composes the data grid (``) @@ -133,6 +173,7 @@ export function Table({ embedded, workspaceId: propWorkspaceId, tableId: propTableId, + viewsEnabled = false, }: TableProps = {}) { const params = useParams() const router = useRouter() @@ -176,11 +217,12 @@ export function Table({ }) const [filter, setFilter] = useState(null) const [filterOpen, setFilterOpen] = useState(false) + /** Hidden **column ids**. Lives here (not in the grid) because the filter + * panel's Columns section edits it and the active view persists it. */ + const [hiddenColumns, setHiddenColumns] = useState([]) - const [{ sort: sortColumn, dir: sortDirection }, setSortParams] = useQueryStates( - tableDetailParsers, - tableDetailUrlKeys - ) + const [{ sort: sortColumn, dir: sortDirection, view: activeViewId }, setTableParams] = + useQueryStates(tableDetailParsers, tableDetailUrlKeys) /** Resolved single-column sort, or `null` when no column is active. */ const sortQuery = useMemo( @@ -278,6 +320,174 @@ export function Table({ queryOptions, }) + const { data: views = NO_VIEWS, isSuccess: viewsLoaded } = useTableViews({ + workspaceId, + tableId, + enabled: viewsEnabled, + }) + const createViewMutation = useCreateTableView({ workspaceId, tableId }) + const updateViewMutation = useUpdateTableView({ workspaceId, tableId }) + const deleteViewMutation = useDeleteTableView({ workspaceId, tableId }) + + /** The selected view, or `null` for the built-in "All" state. A view id that no + * longer resolves (deleted, stale bookmark) falls back to "All" rather than + * rendering an empty view. */ + const activeView = activeViewId ? (views.find((view) => view.id === activeViewId) ?? null) : null + + const [viewModal, setViewModal] = useState(null) + /** Which view id the local filter/sort/hidden state was last seeded from. + * `undefined` means "nothing seeded yet" so the first resolve still runs. */ + const seededViewIdRef = useRef(undefined) + + /** `keepUrlSort` leaves an explicitly deep-linked `?sort=` alone on the first + * seed — the URL is more specific than the view's stored default. */ + const applyViewConfig = useCallback( + (config: TableViewConfig | null, keepUrlSort = false) => { + setFilter(config?.filter ?? null) + setHiddenColumns(config?.hiddenColumns ?? []) + if (keepUrlSort) return + const sortEntry = config?.sort ? Object.entries(config.sort)[0] : undefined + setTableParams({ + sort: sortEntry ? sortEntry[0] : null, + dir: sortEntry ? (sortEntry[1] as SortDirection) : null, + }) + }, + [setTableParams] + ) + + /** + * Resolves the active view and seeds the local filter/sort/hidden-column state + * from it. Runs only when the *selected view id* changes, never on every edit, + * so ad-hoc changes on top of a view are preserved until the user switches away. + * + * On first load with no `?view=` the table's default view (if any) is selected + * and written into the URL explicitly — a link then keeps resolving to the same + * view even after someone changes which view is default. + */ + useEffect(() => { + if (!viewsEnabled || !viewsLoaded) return + + if (seededViewIdRef.current === undefined) { + if (activeViewId === null) { + const defaultView = views.find((view) => view.isDefault) + if (defaultView) { + seededViewIdRef.current = defaultView.id + setTableParams({ view: defaultView.id }) + applyViewConfig(defaultView.config) + return + } + // No view to adopt. Deliberately does NOT apply an empty config — that + // would clear a deep-linked `?sort=` on mount. + seededViewIdRef.current = null + return + } + // A `?view=` that resolves to nothing (deleted view, stale bookmark) falls + // back to "All" without touching state, for the same reason. An explicit + // `?sort=` alongside `?view=` also wins over the view's stored sort. + seededViewIdRef.current = activeView?.id ?? null + if (activeView) applyViewConfig(activeView.config, sortColumn !== null) + return + } + + // A selected id that doesn't resolve yet must not clear anything: right after + // "Save as view" the URL names the new view before the list has refetched, and + // clearing there would wipe the very filter that was just saved. Only an + // explicit switch to "All" (`activeViewId === null`) resets. + if (activeViewId !== null && !activeView) return + + const nextViewId = activeView?.id ?? null + if (seededViewIdRef.current === nextViewId) return + seededViewIdRef.current = nextViewId + applyViewConfig(activeView?.config ?? null) + }, [ + viewsEnabled, + viewsLoaded, + views, + activeView, + activeViewId, + sortColumn, + applyViewConfig, + setTableParams, + ]) + + const currentViewConfig = useMemo( + () => ({ filter, sort: sortQuery, hiddenColumns }), + [filter, sortQuery, hiddenColumns] + ) + + /** + * Whether the live state diverges from what the active view stores (or, on + * "All", whether anything is applied at all). Drives the Save button — it is + * the only affordance that persists, so ad-hoc exploration stays throwaway. + */ + const isViewDirty = activeView + ? !isSameViewConfig(currentViewConfig, activeView.config) + : Boolean(filter) || Boolean(sortQuery) || hiddenColumns.length > 0 + + /** Rename targets a live view rather than a snapshot, so a concurrent rename or + * delete can't leave the modal editing stale data. */ + const renamingView = + viewModal?.mode === 'rename' ? (views.find((v) => v.id === viewModal.viewId) ?? null) : null + + const handleSelectView = useCallback( + (viewId: string | null) => { + setTableParams({ view: viewId }) + }, + [setTableParams] + ) + + const handleRenameView = useCallback((viewId: string) => { + setViewModal({ mode: 'rename', viewId }) + }, []) + + const handleSaveView = () => { + if (activeView) { + updateViewMutation.mutate( + { viewId: activeView.id, config: currentViewConfig }, + { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save view')) } + ) + return + } + setViewModal({ mode: 'create' }) + } + + const handleSubmitViewName = (name: string) => { + if (viewModal?.mode === 'rename') { + updateViewMutation.mutate( + { viewId: viewModal.viewId, name }, + { + onSuccess: () => setViewModal(null), + onError: (error) => toast.error(getErrorMessage(error, 'Failed to rename view')), + } + ) + return + } + createViewMutation.mutate( + { name, config: currentViewConfig }, + { + onSuccess: (view) => { + setViewModal(null) + // Mark as seeded before selecting so the resolve effect doesn't re-apply the config. + seededViewIdRef.current = view.id + setTableParams({ view: view.id }) + }, + onError: (error) => toast.error(getErrorMessage(error, 'Failed to create view')), + } + ) + } + + const handleDeleteView = useCallback( + (viewId: string) => { + deleteViewMutation.mutate(viewId, { + onSuccess: () => { + if (viewId === activeViewId) setTableParams({ view: null }) + }, + onError: (error) => toast.error(getErrorMessage(error, 'Failed to delete view')), + }) + }, + [activeViewId, setTableParams] + ) + const runColumnMutation = useRunColumn({ workspaceId, tableId }) const cancelRunsMutation = useCancelTableRuns({ workspaceId, tableId }) const runColumnMutate = runColumnMutation.mutate @@ -504,14 +714,14 @@ export function Table({ () => ({ options: columnOptions, active: sortColumn ? { column: sortColumn, direction: sortDirection } : null, - onSort: (column, direction) => setSortParams({ sort: column, dir: direction }), + onSort: (column, direction) => setTableParams({ sort: column, dir: direction }), /** * Clearing writes the default direction (stripped by clearOnDefault) and * drops the column, leaving a clean URL with no active sort. */ - onClear: () => setSortParams({ sort: null, dir: DEFAULT_TABLE_DETAIL_SORT_DIRECTION }), + onClear: () => setTableParams({ sort: null, dir: DEFAULT_TABLE_DETAIL_SORT_DIRECTION }), }), - [columnOptions, sortColumn, sortDirection, setSortParams] + [columnOptions, sortColumn, sortDirection, setTableParams] ) const handleFilterApply = (next: Filter | null) => { @@ -685,13 +895,40 @@ export function Table({ sort={sortConfig} filter={filterConfig} aside={ - embedded && (selection.totalRunning > 0 || selection.hasActiveDispatch) ? ( - + <> + {viewsEnabled && ( + <> + + + + )} + {embedded && (selection.totalRunning > 0 || selection.hasActiveDispatch) ? ( + + ) : null} + + } + trailing={ + viewsEnabled && isViewDirty && userPermissions.canEdit ? ( + + {activeView ? 'Save' : 'Save as view'} + ) : undefined } /> @@ -703,6 +940,14 @@ export function Table({ onClose={() => setFilterOpen(false)} /> )} + !open && setViewModal(null)} + mode={viewModal?.mode ?? 'create'} + initialName={renamingView?.name ?? ''} + onSubmit={handleSubmitViewName} + isSubmitting={createViewMutation.isPending || updateViewMutation.isPending} + /> { + const response = await requestJson(listTableViewsContract, { + params: { tableId }, + query: { workspaceId }, + signal, + }) + return response.data.views + }, + enabled: enabled && Boolean(workspaceId && tableId), + staleTime: TABLE_VIEWS_STALE_TIME, + }) +} + +export function useCreateTableView({ workspaceId, tableId }: RowMutationContext) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ name, config }: { name: string; config: TableViewConfigInput }) => { + const response = await requestJson(createTableViewContract, { + params: { tableId }, + body: { workspaceId, name, config }, + }) + return response.data.view + }, + // Seed the new view into the list before the refetch lands, so the URL can + // select it immediately without naming a view the dropdown doesn't have yet. + onSuccess: (view) => { + queryClient.setQueryData(tableKeys.views(tableId), (prev) => + prev ? [...prev, view] : [view] + ) + }, + // Returned so the mutation stays pending until the refetch settles — otherwise + // the Save chip re-enables and flashes dirty against a stale cached config. + onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }), + }) +} + +interface UpdateTableViewParams { + viewId: string + name?: string + config?: TableViewConfigInput + isDefault?: boolean +} + +/** + * Patches one view — overwrite its config, rename it, or promote it to default. + * `isDefault: true` demotes the previous default server-side, so the whole list + * is invalidated rather than just the edited row. + */ +export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ viewId, name, config, isDefault }: UpdateTableViewParams) => { + const response = await requestJson(updateTableViewContract, { + params: { tableId, viewId }, + body: { workspaceId, name, config, isDefault }, + }) + return response.data.view + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }), + }) +} + +export function useDeleteTableView({ workspaceId, tableId }: RowMutationContext) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (viewId: string) => { + await requestJson(deleteTableViewContract, { + params: { tableId, viewId }, + body: { workspaceId }, + }) + return viewId + }, + onSuccess: (viewId) => { + queryClient.setQueryData(tableKeys.views(tableId), (prev) => + prev?.filter((view) => view.id !== viewId) + ) + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }), + }) +} + interface CancelRunsParams { scope: 'all' | 'row' rowId?: string diff --git a/apps/sim/hooks/queries/utils/table-keys.ts b/apps/sim/hooks/queries/utils/table-keys.ts index b1ca6a5fe7b..0721bbfb308 100644 --- a/apps/sim/hooks/queries/utils/table-keys.ts +++ b/apps/sim/hooks/queries/utils/table-keys.ts @@ -12,6 +12,9 @@ export type TableQueryScope = 'active' | 'archived' | 'all' export const TABLE_LIST_STALE_TIME = 30 * 1000 +/** Views change only on explicit user action, so they can sit stale for a while. */ +export const TABLE_VIEWS_STALE_TIME = 60 * 1000 + export const tableKeys = { all: ['tables'] as const, lists: () => [...tableKeys.all, 'list'] as const, @@ -27,6 +30,11 @@ export const tableKeys = { rowWrites: (tableId: string) => [...tableKeys.rowsRoot(tableId), 'write'] as const, find: (tableId: string, paramsKey: string) => [...tableKeys.rowsRoot(tableId), 'find', paramsKey] as const, + /** Deliberately NOT under `detail` — the non-exact `invalidateQueries` on that + * key (row writes, schema changes, rename, job events) would otherwise refetch + * the views list on nearly every table mutation, defeating its staleTime. */ + viewsRoot: () => [...tableKeys.all, 'views'] as const, + views: (tableId: string) => [...tableKeys.viewsRoot(), tableId] as const, activeDispatches: (tableId: string) => [...tableKeys.detail(tableId), 'active-dispatches'] as const, enrichmentDetails: (tableId: string) => diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index c357cc585e8..3fc879544f5 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -12,6 +12,7 @@ import type { TableMetadata, TableRow, TableRowsCursor, + TableViewConfig, } from '@/lib/table' import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' import { CSV_MAX_FILE_SIZE_BYTES } from '@/lib/table/import' @@ -144,6 +145,7 @@ export const tableMetadataSchema = z.object({ columnWidths: z.record(z.string(), z.number().positive()).optional(), columnOrder: z.array(z.string()).optional(), pinnedColumns: z.array(z.string()).optional(), + hiddenColumns: z.array(z.string()).optional(), }) satisfies z.ZodType export const updateTableMetadataBodySchema = z.object({ @@ -1379,3 +1381,114 @@ export const tableEventStreamContract = defineRouteContract({ mode: 'stream', }, }) + +/** + * A saved view's stored shape: `TableMetadata`'s column layout plus the row + * predicate and sort. Every column reference is a stable column id, so a rename + * never invalidates a view. + */ +export const tableViewConfigSchema = tableMetadataSchema.extend({ + filter: filterSchema.nullable().optional(), + sort: domainObjectSchema().nullable().optional(), +}) satisfies z.ZodType + +export const tableViewSchema = z.object({ + id: z.string(), + tableId: z.string(), + name: z.string(), + config: tableViewConfigSchema, + isDefault: z.boolean(), + createdBy: z.string().nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}) + +/** Free-form display label — no character set or length restriction, unlike the + * identifier-shaped table/column names. Only emptiness is rejected, since a + * blank name renders as an unselectable gap in the views dropdown. */ +const viewNameSchema = z.string().trim().min(1, 'View name is required') + +export const tableViewParamsSchema = tableIdParamsSchema.extend({ + viewId: z.string().min(1), +}) + +export const listTableViewsQuerySchema = z.object({ + workspaceId: z.string().min(1, 'Workspace ID is required'), +}) + +export const listTableViewsContract = defineRouteContract({ + method: 'GET', + path: '/api/table/[tableId]/views', + params: tableIdParamsSchema, + query: listTableViewsQuerySchema, + response: { + mode: 'json', + schema: successResponseSchema(z.object({ views: z.array(tableViewSchema) })), + }, +}) + +export const createTableViewBodySchema = z.object({ + workspaceId: z.string().min(1, 'Workspace ID is required'), + name: viewNameSchema, + config: tableViewConfigSchema, +}) + +export const createTableViewContract = defineRouteContract({ + method: 'POST', + path: '/api/table/[tableId]/views', + params: tableIdParamsSchema, + body: createTableViewBodySchema, + response: { + mode: 'json', + schema: successResponseSchema(z.object({ view: tableViewSchema })), + }, +}) + +/** + * Every field is optional so the three call sites — overwrite a view's config, + * rename it, promote it to default — share one endpoint and send only what they + * change. `isDefault: true` demotes the table's existing default server-side. + */ +export const updateTableViewBodySchema = z + .object({ + workspaceId: z.string().min(1, 'Workspace ID is required'), + name: viewNameSchema.optional(), + config: tableViewConfigSchema.optional(), + isDefault: z.boolean().optional(), + }) + .refine( + (value) => + value.name !== undefined || value.config !== undefined || value.isDefault !== undefined, + { message: 'Provide at least one of name, config, or isDefault' } + ) + +export const updateTableViewContract = defineRouteContract({ + method: 'PATCH', + path: '/api/table/[tableId]/views/[viewId]', + params: tableViewParamsSchema, + body: updateTableViewBodySchema, + response: { + mode: 'json', + schema: successResponseSchema(z.object({ view: tableViewSchema })), + }, +}) + +export const deleteTableViewBodySchema = z.object({ + workspaceId: z.string().min(1, 'Workspace ID is required'), +}) + +export const deleteTableViewContract = defineRouteContract({ + method: 'DELETE', + path: '/api/table/[tableId]/views/[viewId]', + params: tableViewParamsSchema, + body: deleteTableViewBodySchema, + response: { + mode: 'json', + schema: successResponseSchema(z.object({ deleted: z.literal(true) })), + }, +}) + +export type TableViewWire = z.output +export type TableViewConfigInput = z.input +export type CreateTableViewBody = z.input +export type UpdateTableViewBody = z.input diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 80c9081dcc2..f3dd254fec4 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -473,6 +473,7 @@ export const env = createEnv({ DATA_DRAINS_ENABLED: z.boolean().optional(), // Enable data drains on self-hosted (bypasses hosted requirements) FORKING_ENABLED: z.boolean().optional(), // Enable workspace forking on self-hosted (bypasses hosted requirements) DEPLOY_AS_BLOCK: z.boolean().optional(), // Enable deploy-as-block (publish a workflow as a reusable org-wide custom block) + TABLE_VIEWS: z.boolean().optional(), // Enable saved table views (named filter/sort/column-visibility presets) and the column show/hide menu // Organizations - for self-hosted deployments ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts index aae8a7a2b6b..34aa45cf0b3 100644 --- a/apps/sim/lib/core/config/feature-flags.test.ts +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -161,6 +161,30 @@ describe('isFeatureEnabled', () => { }) }) + describe('table-views flag', () => { + it('falls back to TABLE_VIEWS when AppConfig is disabled', async () => { + envRef.TABLE_VIEWS = undefined + expect(await isFeatureEnabled('table-views', { userId: 'u1', orgId: 'o1' })).toBe(false) + + envRef.TABLE_VIEWS = true + expect(await isFeatureEnabled('table-views', { userId: 'u1', orgId: 'o1' })).toBe(true) + }) + + it('targets specific orgs and users via AppConfig, ignoring the fallback secret', async () => { + envRef.TABLE_VIEWS = undefined + withAppConfig({ 'table-views': { orgIds: ['o1'], userIds: ['u9'] } }) + expect(await isFeatureEnabled('table-views', { orgId: 'o1' })).toBe(true) + expect(await isFeatureEnabled('table-views', { userId: 'u9' })).toBe(true) + expect(await isFeatureEnabled('table-views', { userId: 'u1', orgId: 'o2' })).toBe(false) + }) + + it('resolves off with no context, so a signed-out render never gates on', async () => { + envRef.TABLE_VIEWS = undefined + withAppConfig({ 'table-views': { orgIds: ['o1'] } }) + expect(await isFeatureEnabled('table-views')).toBe(false) + }) + }) + it('returns false for an unknown flag', async () => { withAppConfig({}) expect(await enabled('missing', { userId: 'u1' })).toBe(false) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 558707fa587..21a98e93824 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -112,6 +112,17 @@ const FEATURE_FLAGS = { 'custom-block publish/list routes. Off-AppConfig falls back to DEPLOY_AS_BLOCK.', fallback: 'DEPLOY_AS_BLOCK', }, + 'table-views': { + description: + 'Saved table views (named filter/sort/column-visibility presets) plus the column show/hide ' + + 'menu, in the table-detail options bar. UI-only gate: resolved in the table page (server) ' + + "and passed down, so the table falls back to today's Filter/Sort bar when off. The routes " + + 'and the table_views table ship ungated — they are inert with no UI to call them, and a view ' + + 'saved during a rollout must survive the flag being toggled back off. Embedded (mothership) ' + + 'tables render without views regardless, since no server context resolves the flag there. ' + + 'Off-AppConfig falls back to TABLE_VIEWS.', + fallback: 'TABLE_VIEWS', + }, } satisfies Record /** diff --git a/apps/sim/lib/table/index.ts b/apps/sim/lib/table/index.ts index 58a73f2313f..b4352ff2b21 100644 --- a/apps/sim/lib/table/index.ts +++ b/apps/sim/lib/table/index.ts @@ -20,4 +20,5 @@ export * from '@/lib/table/service' export * from '@/lib/table/sql' export * from '@/lib/table/types' export * from '@/lib/table/validation' +export * from '@/lib/table/views/service' export * from '@/lib/table/workflow-groups/service' diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index 384ca5afb7e..d72a1fe4580 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -239,8 +239,8 @@ export interface TableSchema { /** * Table-level metadata stored alongside the table definition. UI state only - * (column widths, column order, pinned columns) — workflow-group concurrency - * is enforced at the trigger.dev queue layer, not via metadata. + * (column widths, column order, pinned columns, hidden columns) — workflow-group + * concurrency is enforced at the trigger.dev queue layer, not via metadata. */ export interface TableMetadata { /** Pixel widths keyed by **column id** (`getColumnId`). */ @@ -249,6 +249,26 @@ export interface TableMetadata { columnOrder?: string[] /** **Column ids** pinned to the left while scrolling horizontally. */ pinnedColumns?: string[] + /** + * **Column ids** hidden from the grid. A deny-list, so a column added later is + * visible by default instead of needing to be re-enabled everywhere. Hiding is + * render-only — rows still arrive as whole JSONB blobs, so a hidden column's + * data is retained and reappears intact when it is unhidden. + */ + hiddenColumns?: string[] +} + +/** + * The saved shape of a table view: everything `TableMetadata` covers plus the + * row predicate and sort. Stored in `table_views.config`. + * + * `filter`/`sort` are what the view builder persists explicitly; the layout keys + * are inherited from `TableMetadata` and auto-save into the active view as the + * user resizes, reorders, pins, or hides columns. + */ +export interface TableViewConfig extends TableMetadata { + filter?: Filter | null + sort?: Sort | null } /** Async background-job lifecycle state for a table. NULL/undefined = idle (no job). */ diff --git a/apps/sim/lib/table/views/service.test.ts b/apps/sim/lib/table/views/service.test.ts new file mode 100644 index 00000000000..5b048954313 --- /dev/null +++ b/apps/sim/lib/table/views/service.test.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { ColumnDefinition, TableViewConfig } from '@/lib/table/types' +import { pruneViewConfig } from '@/lib/table/views/service' + +const columns: ColumnDefinition[] = [ + { id: 'col_a', name: 'Name', type: 'text' }, + { id: 'col_b', name: 'Email', type: 'text' }, +] + +describe('pruneViewConfig', () => { + it('drops layout references to columns that no longer exist', () => { + const config: TableViewConfig = { + columnOrder: ['col_a', 'col_gone', 'col_b'], + pinnedColumns: ['col_gone'], + hiddenColumns: ['col_b', 'col_gone'], + columnWidths: { col_a: 200, col_gone: 120 }, + } + + expect(pruneViewConfig(config, columns)).toEqual({ + columnOrder: ['col_a', 'col_b'], + pinnedColumns: [], + hiddenColumns: ['col_b'], + columnWidths: { col_a: 200 }, + }) + }) + + it('drops a sort on a deleted column and collapses to null when none remain', () => { + expect(pruneViewConfig({ sort: { col_gone: 'asc' } }, columns).sort).toBeNull() + expect(pruneViewConfig({ sort: { col_a: 'desc' } }, columns).sort).toEqual({ col_a: 'desc' }) + }) + + it('leaves the filter untouched even when it references a deleted column', () => { + // Pruning a predicate would silently widen the view's row set — surfacing a + // stale condition the user can see and remove is the safer failure. + const filter = { col_gone: { $eq: 'x' } } + expect(pruneViewConfig({ filter }, columns).filter).toEqual(filter) + }) + + it('leaves absent keys absent rather than materializing empty ones', () => { + expect(pruneViewConfig({}, columns)).toEqual({}) + }) + + it('falls back to column name for legacy columns with no id', () => { + const legacy: ColumnDefinition[] = [{ name: 'Legacy', type: 'text' }] + expect(pruneViewConfig({ hiddenColumns: ['Legacy', 'nope'] }, legacy).hiddenColumns).toEqual([ + 'Legacy', + ]) + }) +}) diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts new file mode 100644 index 00000000000..857233e8f48 --- /dev/null +++ b/apps/sim/lib/table/views/service.ts @@ -0,0 +1,200 @@ +/** + * Saved views on a user table — named presets of `{ filter, sort, column layout }`. + * + * A view is presentation state, never an access boundary: it narrows what a + * reader sees by default, but every row it hides is still reachable by switching + * to "All". Row access is enforced entirely by the caller's workspace permission. + * + * "All" is the *absence* of a view, so no row is seeded per table and a table is + * always reachable unfiltered even if every saved view is broken or deleted. + */ + +import { db } from '@sim/db' +import { tableViews } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, ne } from 'drizzle-orm' +import { getColumnId } from '@/lib/table/column-keys' +import type { ColumnDefinition, TableViewConfig } from '@/lib/table/types' + +const logger = createLogger('TableViewsService') + +/** A saved view as returned to clients. */ +export interface TableView { + id: string + tableId: string + name: string + config: TableViewConfig + isDefault: boolean + createdBy: string | null + createdAt: Date + updatedAt: Date +} + +/** Raised when a view operation fails a user-correctable precondition. */ +export class TableViewValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'TableViewValidationError' + } +} + +/** + * Drops references to columns that no longer exist from a stored config. + * + * `table_views.config` is a JSON blob with no foreign keys to the table schema, + * so deleting a column leaves dangling ids behind. Rather than fan out writes to + * every view on every column delete, stale ids are pruned here on read — the + * stored blob stays as-is and self-heals on the next save. + * + * `filter` is deliberately left untouched: pruning a predicate would silently + * widen the view's row set, which is worse than surfacing a filter the user can + * see and remove. The filter builder already renders a stale column id as-is. + */ +export function pruneViewConfig( + config: TableViewConfig, + columns: ColumnDefinition[] +): TableViewConfig { + const live = new Set(columns.map(getColumnId)) + const pruned: TableViewConfig = { ...config } + + if (config.columnOrder) pruned.columnOrder = config.columnOrder.filter((id) => live.has(id)) + if (config.pinnedColumns) pruned.pinnedColumns = config.pinnedColumns.filter((id) => live.has(id)) + if (config.hiddenColumns) pruned.hiddenColumns = config.hiddenColumns.filter((id) => live.has(id)) + if (config.columnWidths) { + const widths: Record = {} + for (const [id, width] of Object.entries(config.columnWidths)) { + if (live.has(id)) widths[id] = width + } + pruned.columnWidths = widths + } + if (config.sort) { + const sort: Record = {} + for (const [id, direction] of Object.entries(config.sort)) { + if (live.has(id)) sort[id] = direction + } + pruned.sort = Object.keys(sort).length > 0 ? sort : null + } + + return pruned +} + +function toTableView(row: typeof tableViews.$inferSelect, columns: ColumnDefinition[]): TableView { + return { + id: row.id, + tableId: row.tableId, + name: row.name, + config: pruneViewConfig((row.config ?? {}) as TableViewConfig, columns), + isDefault: row.isDefault, + createdBy: row.createdBy, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } +} + +/** Every view on a table, oldest first, with stale column references pruned. */ +export async function listTableViews( + tableId: string, + columns: ColumnDefinition[] +): Promise { + const rows = await db + .select() + .from(tableViews) + .where(eq(tableViews.tableId, tableId)) + .orderBy(asc(tableViews.createdAt), asc(tableViews.id)) + + return rows.map((row) => toTableView(row, columns)) +} + +function normalizeName(name: string): string { + const trimmed = name.trim() + if (!trimmed) throw new TableViewValidationError('View name cannot be empty') + return trimmed +} + +export interface CreateTableViewData { + tableId: string + workspaceId: string + name: string + config: TableViewConfig + userId: string + columns: ColumnDefinition[] +} + +export async function createTableView(data: CreateTableViewData): Promise { + const name = normalizeName(data.name) + + const [row] = await db + .insert(tableViews) + .values({ + id: generateId(), + tableId: data.tableId, + workspaceId: data.workspaceId, + name, + config: data.config, + createdBy: data.userId, + }) + .returning() + + logger.info('Created table view', { tableId: data.tableId, viewId: row.id }) + return toTableView(row, data.columns) +} + +export interface UpdateTableViewData { + viewId: string + tableId: string + name?: string + config?: TableViewConfig + isDefault?: boolean + columns: ColumnDefinition[] +} + +/** + * Patches a view. `isDefault: true` clears the table's existing default in the + * same transaction — the `table_views_table_default_unique` partial index rejects + * a second default, so the clear cannot be skipped or reordered after the set. + */ +export async function updateTableView(data: UpdateTableViewData): Promise { + const patch: Partial = { updatedAt: new Date() } + if (data.name !== undefined) patch.name = normalizeName(data.name) + if (data.config !== undefined) patch.config = data.config + if (data.isDefault !== undefined) patch.isDefault = data.isDefault + + const row = await db.transaction(async (tx) => { + if (data.isDefault === true) { + await tx + .update(tableViews) + .set({ isDefault: false }) + .where( + and( + eq(tableViews.tableId, data.tableId), + eq(tableViews.isDefault, true), + ne(tableViews.id, data.viewId) + ) + ) + } + + const [updated] = await tx + .update(tableViews) + .set(patch) + .where(and(eq(tableViews.id, data.viewId), eq(tableViews.tableId, data.tableId))) + .returning() + + return updated + }) + + if (!row) throw new TableViewValidationError('View not found') + + return toTableView(row, data.columns) +} + +/** Deleting the default simply leaves the table on "All". */ +export async function deleteTableView(viewId: string, tableId: string): Promise { + const deleted = await db + .delete(tableViews) + .where(and(eq(tableViews.id, viewId), eq(tableViews.tableId, tableId))) + .returning({ id: tableViews.id }) + + if (deleted.length > 0) logger.info('Deleted table view', { tableId, viewId }) + return deleted.length > 0 +} diff --git a/packages/db/migrations/0270_table_views.sql b/packages/db/migrations/0270_table_views.sql new file mode 100644 index 00000000000..fba5cc72377 --- /dev/null +++ b/packages/db/migrations/0270_table_views.sql @@ -0,0 +1,17 @@ +CREATE TABLE "table_views" ( + "id" text PRIMARY KEY NOT NULL, + "table_id" text NOT NULL, + "workspace_id" text NOT NULL, + "name" text NOT NULL, + "config" jsonb DEFAULT '{}' NOT NULL, + "is_default" boolean DEFAULT false NOT NULL, + "created_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "table_views" ADD CONSTRAINT "table_views_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_views" ADD CONSTRAINT "table_views_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "table_views" ADD CONSTRAINT "table_views_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "table_views_table_created_idx" ON "table_views" USING btree ("table_id","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "table_views_table_default_unique" ON "table_views" USING btree ("table_id") WHERE is_default = true; \ No newline at end of file diff --git a/packages/db/migrations/meta/0270_snapshot.json b/packages/db/migrations/meta/0270_snapshot.json new file mode 100644 index 00000000000..5813418f567 --- /dev/null +++ b/packages/db/migrations/meta/0270_snapshot.json @@ -0,0 +1,17829 @@ +{ + "id": "ed723492-796b-46e2-a823-21283a3e4bb7", + "prevId": "dea711e3-b8c7-4279-bb8a-353bdaffdfe8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_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.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_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_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_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": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_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 + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index cde0fe20a26..810655c20b6 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1884,6 +1884,13 @@ "when": 1784911198876, "tag": "0269_late_spencer_smythe", "breakpoints": true + }, + { + "idx": 270, + "version": "7", + "when": 1784954793179, + "tag": "0270_table_views", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index a13d0e01eee..0afce287b2c 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3767,6 +3767,54 @@ export const userTableRows = pgTable( }) ) +/** + * Saved presets for a user-defined table — a named filter + sort + column layout. + * Workspace-shared: anyone who can read the table sees every view, and `write` is + * required to create, update, or delete one. The absence of a view is the built-in + * "All" state, so a table is always reachable unfiltered without a seeded row. + * + * A dedicated table rather than a key on `user_table_definitions.metadata`: that + * column is written read-modify-write with a shallow merge, so a stale snapshot + * from any concurrent column resize would silently drop a view saved in between. + * Per-view rows make each save an independent insert. + */ +export const tableViews = pgTable( + 'table_views', + { + id: text('id').primaryKey(), + tableId: text('table_id') + .notNull() + .references(() => userTableDefinitions.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + /** + * @remarks + * `TableViewConfig` — `{ filter, sort, hiddenColumns, columnOrder, columnWidths, + * pinnedColumns }`. Every column reference is keyed by stable column id, so a + * rename never touches a saved view; ids of deleted columns are pruned on read. + */ + config: jsonb('config').notNull().default('{}'), + isDefault: boolean('is_default').notNull().default(false), + /** + * Nullable with `set null` rather than the `cascade` used for table ownership: + * a view is workspace-shared, so it must outlive the member who created it. + */ + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + /** Covers the only read path: list a table's views in creation order. */ + tableCreatedIdx: index('table_views_table_created_idx').on(table.tableId, table.createdAt), + /** At most one default view per table, enforced in the DB. */ + defaultViewUnique: uniqueIndex('table_views_table_default_unique') + .on(table.tableId) + .where(sql`is_default = true`), + }) +) + /** * Background data-mutation jobs on a user table (CSV import, bulk filtered delete). One row per * job. A detached worker streams progress into `rows_processed` and flips `status` to a terminal diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 63a5a59b65f..1712cdaa9f5 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: 975, - zodRoutes: 975, + totalRoutes: 977, + zodRoutes: 977, nonZodRoutes: 0, } as const From 308a44808a5b198a2b699a2ca6b9bc51a27fae2b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 00:16:52 -0700 Subject: [PATCH 02/33] fix(tables): views own column layout, preserve deep-linked sort, seed update cache --- .../components/table-grid/table-grid.tsx | 59 ++++++++++++------- .../[workspaceId]/tables/[tableId]/table.tsx | 26 +++++++- apps/sim/hooks/queries/tables.ts | 7 +++ 3 files changed, 68 insertions(+), 24 deletions(-) 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 51c8dd49a97..2f4acd3e527 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 @@ -10,7 +10,13 @@ import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import type { RunLimit, RunMode, TableFindMatch } from '@/lib/api/contracts/tables' import { captureEvent } from '@/lib/posthog/client' -import type { ColumnDefinition, Filter, TableRow as TableRowType, WorkflowGroup } from '@/lib/table' +import type { + ColumnDefinition, + Filter, + TableMetadata, + TableRow as TableRowType, + WorkflowGroup, +} from '@/lib/table' import { getColumnId } from '@/lib/table/column-keys' import { TABLE_LIMITS } from '@/lib/table/constants' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -214,6 +220,16 @@ interface TableGridProps { * panel's Columns section edits the same list and the active view persists it. */ hiddenColumns?: string[] + /** Active view's stored layout. When set it owns column order/width/pinning + * instead of the table's shared `metadata`. */ + viewLayout?: TableMetadata | null + /** Identity of `viewLayout`'s source (the view id, or `null` for "All"). Changing + * it re-seeds the grid — comparing the config object itself would re-seed on + * every refetch. */ + viewLayoutKey?: string | null + /** Routes layout writes to the active view. Falls back to the table-metadata + * mutation when absent. */ + onPersistLayout?: (patch: TableMetadata) => void /** * Ref the grid populates with its `handleColumnRename` so the wrapper's * sidebars can fire a column rename back into the grid (rewrites local @@ -313,6 +329,9 @@ export function TableGrid({ onSelectionChange, queryOptions, hiddenColumns, + viewLayout, + viewLayoutKey = null, + onPersistLayout, columnRenameSinkRef, afterDeleteRowsSinkRef, afterDeleteAllSinkRef, @@ -370,6 +389,8 @@ export function TableGrid({ const pinnedColumnsRef = useRef(pinnedColumns) pinnedColumnsRef.current = pinnedColumns const metadataSeededRef = useRef(false) + /** Which layout source the grid last seeded from, so a view switch re-seeds. */ + const seededLayoutKeyRef = useRef(null) const containerRef = useRef(null) const scrollRef = useRef(null) const theadRef = useRef(null) @@ -1735,32 +1756,28 @@ export function TableGrid({ }, [tableData?.id]) useEffect(() => { - if (!tableData?.metadata) return - if ( - !tableData.metadata.columnWidths && - !tableData.metadata.columnOrder && - !tableData.metadata.pinnedColumns - ) + // With a view active its config owns the layout; otherwise the table's own + // metadata does. Switching views re-seeds unconditionally so the incoming + // view's layout replaces the outgoing one. + const source = viewLayout ?? tableData?.metadata + const switchedView = viewLayoutKey !== seededLayoutKeyRef.current + if (!source) return + if (!source.columnWidths && !source.columnOrder && !source.pinnedColumns && !switchedView) return - // First load: seed all from the server and remember we've seeded. - if (!metadataSeededRef.current) { + + if (!metadataSeededRef.current || switchedView) { metadataSeededRef.current = true - if (tableData.metadata.columnWidths) { - setColumnWidths(tableData.metadata.columnWidths) - } - if (tableData.metadata.columnOrder) { - setColumnOrder(tableData.metadata.columnOrder) - } - if (tableData.metadata.pinnedColumns) { - setPinnedColumns(tableData.metadata.pinnedColumns) - } + seededLayoutKeyRef.current = viewLayoutKey + setColumnWidths(source.columnWidths ?? {}) + setColumnOrder(source.columnOrder ?? null) + setPinnedColumns(source.pinnedColumns ?? []) return } // After first load: only re-seed `columnOrder` when the *set of columns* // changes (e.g. a workflow group adds/removes outputs server-side). Pure // reorders are left alone so an in-flight optimistic drag isn't clobbered // by a refetch returning the pre-drag order. - const serverOrder = tableData.metadata.columnOrder + const serverOrder = source.columnOrder if (serverOrder) { const localOrder = columnOrderRef.current const serverSet = new Set(serverOrder) @@ -1771,7 +1788,7 @@ export function TableGrid({ setColumnOrder(serverOrder) } } - }, [tableData?.metadata]) + }, [tableData?.metadata, viewLayout, viewLayoutKey]) useEffect(() => { if (!isColumnSelection || !selectionAnchor) return @@ -2057,7 +2074,7 @@ export function TableGrid({ batchUpdateAsyncRef.current = batchUpdateRowsMutation.mutateAsync const updateMetadataRef = useRef(updateMetadataMutation.mutate) - updateMetadataRef.current = updateMetadataMutation.mutate + updateMetadataRef.current = onPersistLayout ?? updateMetadataMutation.mutate const deleteWorkflowGroupRef = useRef(deleteWorkflowGroupMutation.mutate) deleteWorkflowGroupRef.current = deleteWorkflowGroupMutation.mutate diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 976b427f643..68b9a18c699 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -15,6 +15,7 @@ import type { Filter, Sort, SortDirection, + TableMetadata, TableRow as TableRowType, TableViewConfig, WorkflowGroup, @@ -373,7 +374,7 @@ export function Table({ if (defaultView) { seededViewIdRef.current = defaultView.id setTableParams({ view: defaultView.id }) - applyViewConfig(defaultView.config) + applyViewConfig(defaultView.config, sortColumn !== null) return } // No view to adopt. Deliberately does NOT apply an empty config — that @@ -410,9 +411,11 @@ export function Table({ setTableParams, ]) + /** A view update replaces `config` wholesale, so the layout the grid auto-saves + * is spread back in — otherwise Save would drop it. */ const currentViewConfig = useMemo( - () => ({ filter, sort: sortQuery, hiddenColumns }), - [filter, sortQuery, hiddenColumns] + () => ({ ...activeView?.config, filter, sort: sortQuery, hiddenColumns }), + [activeView, filter, sortQuery, hiddenColumns] ) /** @@ -440,6 +443,20 @@ export function Table({ setViewModal({ mode: 'rename', viewId }) }, []) + /** Column order/width/pinning auto-saves into the active view as the user drags, + * which is why `isSameViewConfig` excludes layout from the dirty check. With no + * view active the grid keeps writing the table's shared metadata. */ + const handlePersistLayout = useCallback( + (patch: TableMetadata) => { + if (!activeView) return + updateViewMutation.mutate( + { viewId: activeView.id, config: { ...activeView.config, ...patch } }, + { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) } + ) + }, + [activeView] + ) + const handleSaveView = () => { if (activeView) { updateViewMutation.mutate( @@ -972,6 +989,9 @@ export function Table({ onSelectionChange={onSelectionChange} queryOptions={queryOptions} hiddenColumns={hiddenColumns} + viewLayout={activeView?.config ?? null} + viewLayoutKey={activeView?.id ?? null} + onPersistLayout={activeView ? handlePersistLayout : undefined} columnRenameSinkRef={columnRenameSinkRef} afterDeleteRowsSinkRef={afterDeleteRowsSinkRef} afterDeleteAllSinkRef={afterDeleteAllSinkRef} diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 8c18d41ac56..f42bb721556 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -1339,6 +1339,13 @@ export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext) }) return response.data.view }, + // Without this the edited view's cached config stays stale until the refetch, + // so `isViewDirty` re-reads true and the Save chip flashes back after a save. + onSuccess: (view) => { + queryClient.setQueryData(tableKeys.views(tableId), (prev) => + prev?.map((existing) => (existing.id === view.id ? view : existing)) + ) + }, onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }), }) } From f4e3b07de0fd476a7001ac2808aa58c7035bd5ae Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 00:27:33 -0700 Subject: [PATCH 03/33] fix(tables): merge view layout writes server-side, keep layout on save-from-All --- .../table/[tableId]/views/[viewId]/route.ts | 12 ++++++++-- .../[workspaceId]/tables/[tableId]/table.tsx | 23 +++++++++++++------ apps/sim/hooks/queries/tables.ts | 7 ++++-- apps/sim/lib/api/contracts/tables.ts | 17 ++++++++++++-- apps/sim/lib/table/views/service.ts | 12 +++++++++- 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts index 3bfefb7fa66..50cffe69f8f 100644 --- a/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts @@ -32,7 +32,7 @@ export const PATCH = withRouteHandler( if (!parsed.success) return parsed.response const { tableId, viewId } = parsed.data.params - const { workspaceId, name, config, isDefault } = parsed.data.body + const { workspaceId, name, config, configPatch, isDefault } = parsed.data.body const result = await checkAccess(tableId, authResult.userId, 'write') if (!result.ok) return accessError(result, requestId, tableId) @@ -42,7 +42,15 @@ export const PATCH = withRouteHandler( } const columns = (result.table.schema as TableSchema).columns ?? [] - const view = await updateTableView({ viewId, tableId, name, config, isDefault, columns }) + const view = await updateTableView({ + viewId, + tableId, + name, + config, + configPatch, + isDefault, + columns, + }) return NextResponse.json({ success: true, data: { view } }) } catch (error) { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 68b9a18c699..dcae047ac9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -411,11 +411,18 @@ export function Table({ setTableParams, ]) - /** A view update replaces `config` wholesale, so the layout the grid auto-saves - * is spread back in — otherwise Save would drop it. */ + /** Save replaces `config` wholesale, so the current layout is spread back in. + * With a view active that's the view's own stored layout; on "All" it's the + * table metadata the grid is currently rendering from — without which "Save as + * view" would create a layout-less view and then reset the grid to defaults. */ const currentViewConfig = useMemo( - () => ({ ...activeView?.config, filter, sort: sortQuery, hiddenColumns }), - [activeView, filter, sortQuery, hiddenColumns] + () => ({ + ...(activeView?.config ?? tableData?.metadata), + filter, + sort: sortQuery, + hiddenColumns, + }), + [activeView, tableData?.metadata, filter, sortQuery, hiddenColumns] ) /** @@ -444,13 +451,15 @@ export function Table({ }, []) /** Column order/width/pinning auto-saves into the active view as the user drags, - * which is why `isSameViewConfig` excludes layout from the dirty check. With no - * view active the grid keeps writing the table's shared metadata. */ + * which is why `isSameViewConfig` excludes layout from the dirty check. Sent as + * a `configPatch` so the server merges it — two overlapping layout writes must + * not each replace the whole blob from their own snapshot. With no view active + * the grid keeps writing the table's shared metadata. */ const handlePersistLayout = useCallback( (patch: TableMetadata) => { if (!activeView) return updateViewMutation.mutate( - { viewId: activeView.id, config: { ...activeView.config, ...patch } }, + { viewId: activeView.id, configPatch: patch }, { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) } ) }, diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index f42bb721556..520553c28b5 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -1319,7 +1319,10 @@ export function useCreateTableView({ workspaceId, tableId }: RowMutationContext) interface UpdateTableViewParams { viewId: string name?: string + /** Full replace (explicit Save). Mutually exclusive with `configPatch`. */ config?: TableViewConfigInput + /** Server-side shallow merge — used for the grid's incremental layout writes. */ + configPatch?: TableViewConfigInput isDefault?: boolean } @@ -1332,10 +1335,10 @@ export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext) const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ viewId, name, config, isDefault }: UpdateTableViewParams) => { + mutationFn: async ({ viewId, name, config, configPatch, isDefault }: UpdateTableViewParams) => { const response = await requestJson(updateTableViewContract, { params: { tableId, viewId }, - body: { workspaceId, name, config, isDefault }, + body: { workspaceId, name, config, configPatch, isDefault }, }) return response.data.view }, diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index 3fc879544f5..39302476ba9 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -1453,14 +1453,27 @@ export const updateTableViewBodySchema = z .object({ workspaceId: z.string().min(1, 'Workspace ID is required'), name: viewNameSchema.optional(), + /** Full replace. Use for an explicit Save, where dropping a removed filter is the point. */ config: tableViewConfigSchema.optional(), + /** + * Shallow-merged into the stored config server-side (jsonb `||`), so concurrent + * partial writes can't clobber each other from a stale client snapshot. Use for + * the grid's incremental layout saves. + */ + configPatch: tableViewConfigSchema.optional(), isDefault: z.boolean().optional(), }) .refine( (value) => - value.name !== undefined || value.config !== undefined || value.isDefault !== undefined, - { message: 'Provide at least one of name, config, or isDefault' } + value.name !== undefined || + value.config !== undefined || + value.configPatch !== undefined || + value.isDefault !== undefined, + { message: 'Provide at least one of name, config, configPatch, or isDefault' } ) + .refine((value) => !(value.config !== undefined && value.configPatch !== undefined), { + message: 'config and configPatch are mutually exclusive', + }) export const updateTableViewContract = defineRouteContract({ method: 'PATCH', diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts index 857233e8f48..72c1f4adf7c 100644 --- a/apps/sim/lib/table/views/service.ts +++ b/apps/sim/lib/table/views/service.ts @@ -13,7 +13,7 @@ import { db } from '@sim/db' import { tableViews } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { and, asc, eq, ne } from 'drizzle-orm' +import { and, asc, eq, ne, sql } from 'drizzle-orm' import { getColumnId } from '@/lib/table/column-keys' import type { ColumnDefinition, TableViewConfig } from '@/lib/table/types' @@ -144,7 +144,10 @@ export interface UpdateTableViewData { viewId: string tableId: string name?: string + /** Full replace — an explicit Save, where removing a filter must persist. */ config?: TableViewConfig + /** Shallow-merged into the stored config. Mutually exclusive with `config`. */ + configPatch?: TableViewConfig isDefault?: boolean columns: ColumnDefinition[] } @@ -153,11 +156,18 @@ export interface UpdateTableViewData { * Patches a view. `isDefault: true` clears the table's existing default in the * same transaction — the `table_views_table_default_unique` partial index rejects * a second default, so the clear cannot be skipped or reordered after the set. + * + * `configPatch` merges in the database (`||`) rather than client-side, so two + * overlapping partial writes — a column resize landing while a pin is in flight — + * can't each replace the whole blob from their own stale snapshot. */ export async function updateTableView(data: UpdateTableViewData): Promise { const patch: Partial = { updatedAt: new Date() } if (data.name !== undefined) patch.name = normalizeName(data.name) if (data.config !== undefined) patch.config = data.config + if (data.configPatch !== undefined) { + patch.config = sql`${tableViews.config} || ${JSON.stringify(data.configPatch)}::jsonb` + } if (data.isDefault !== undefined) patch.isDefault = data.isDefault const row = await db.transaction(async (tx) => { From 9fb054c379d5123078c83a2d173ee457670abc1b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 00:31:15 -0700 Subject: [PATCH 04/33] fix(tables): send view saves as a merge patch so concurrent writes can't clobber --- .../[workspaceId]/tables/[tableId]/table.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index dcae047ac9d..6ee7f81412f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -411,10 +411,11 @@ export function Table({ setTableParams, ]) - /** Save replaces `config` wholesale, so the current layout is spread back in. - * With a view active that's the view's own stored layout; on "All" it's the - * table metadata the grid is currently rendering from — without which "Save as - * view" would create a layout-less view and then reset the grid to defaults. */ + /** The payload for creating a view, and the left-hand side of the dirty check. + * Carries the current layout so "Save as view" from "All" captures the widths / + * order / pins the grid is rendering (they live in the table's shared metadata + * until a view owns them) instead of creating a layout-less view that then + * resets the grid. Updates never send this — they send a merge patch. */ const currentViewConfig = useMemo( () => ({ ...(activeView?.config ?? tableData?.metadata), @@ -468,8 +469,12 @@ export function Table({ const handleSaveView = () => { if (activeView) { + // Only the fields Save owns, merged server-side — never a client-built full + // config. A full replace from a cached snapshot would drop a layout write + // still in flight (and vice versa). `null`/`[]` merge as explicit values, so + // clearing a filter or unhiding every column still persists as a removal. updateViewMutation.mutate( - { viewId: activeView.id, config: currentViewConfig }, + { viewId: activeView.id, configPatch: { filter, sort: sortQuery, hiddenColumns } }, { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save view')) } ) return From 8ab2d52e68f147bea53cb22d0f8ea8bbe0a25844 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 00:39:18 -0700 Subject: [PATCH 05/33] fix(tables): persist explicit All selection, prune view state for deleted columns --- .../tables/[tableId]/search-params.ts | 20 ++++--- .../[workspaceId]/tables/[tableId]/table.tsx | 54 +++++++++++++++---- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts index 1b6b9b97643..5b510dc38e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/search-params.ts @@ -22,18 +22,26 @@ export const tableDetailParsers = { sort: parseAsString, dir: parseAsStringLiteral(SORT_DIRECTIONS).withDefault(DEFAULT_TABLE_DETAIL_SORT_DIRECTION), /** - * Active saved view id. Nullable with no default: `null` is the built-in "All" - * state (no view), which is behaviourally distinct from any saved view and is - * what a table with zero views always shows. + * Active view, as a tri-state: + * - absent (`null`) — nothing chosen yet, so the table's default view is adopted + * - {@link ALL_VIEW_PARAM} — "All" chosen *explicitly*; never overridden by a default + * - any other value — a saved view id + * + * The sentinel exists because clearing the param and explicitly picking "All" + * would otherwise be the same URL, so a default view would silently reclaim the + * table on every reload. * * Only the id lives here — the view's filter/sort/layout are looked up from the - * loaded list, per the store-the-id-derive-the-object convention. A table's - * default view is resolved on mount and written back explicitly, so a shared - * link keeps pointing at the same view even if someone changes the default. + * loaded list, per the store-the-id-derive-the-object convention. A default view + * is written back explicitly on adoption, so a shared link keeps resolving to the + * same view even if someone later changes which one is default. */ view: parseAsString, } as const +/** Sentinel for an explicit "All" selection. See `tableDetailParsers.view`. */ +export const ALL_VIEW_PARAM = 'all' + /** * Sort + view state: clean URLs, no back-stack churn. * diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 6ee7f81412f..fb096c9d571 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -72,6 +72,7 @@ import { COLUMN_SIDEBAR_WIDTH } from './components/table-grid/constants' import { COLUMN_TYPE_ICONS } from './components/table-grid/headers' import { useTable, useTableEventStream } from './hooks' import { + ALL_VIEW_PARAM, DEFAULT_TABLE_DETAIL_SORT_DIRECTION, tableDetailParsers, tableDetailUrlKeys, @@ -382,6 +383,10 @@ export function Table({ seededViewIdRef.current = null return } + if (activeViewId === ALL_VIEW_PARAM) { + seededViewIdRef.current = null + return + } // A `?view=` that resolves to nothing (deleted view, stale bookmark) falls // back to "All" without touching state, for the same reason. An explicit // `?sort=` alongside `?view=` also wins over the view's stored sort. @@ -394,7 +399,7 @@ export function Table({ // "Save as view" the URL names the new view before the list has refetched, and // clearing there would wipe the very filter that was just saved. Only an // explicit switch to "All" (`activeViewId === null`) resets. - if (activeViewId !== null && !activeView) return + if (activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && !activeView) return const nextViewId = activeView?.id ?? null if (seededViewIdRef.current === nextViewId) return @@ -411,6 +416,30 @@ export function Table({ setTableParams, ]) + /** + * Live state pruned the same way `pruneViewConfig` prunes the stored config on + * read. Without this, deleting a hidden or sorted column leaves the local ids + * behind while the server drops them, so the dirty check never balances again — + * Save writes the stale id, the response comes back pruned, and the chip is + * stuck on. Guarded on the schema being loaded so an empty first render doesn't + * prune everything. + */ + const liveColumnIds = useMemo(() => new Set(columns.map(getColumnId)), [columns]) + const effectiveHiddenColumns = useMemo( + () => + columns.length === 0 ? hiddenColumns : hiddenColumns.filter((id) => liveColumnIds.has(id)), + [columns.length, hiddenColumns, liveColumnIds] + ) + const effectiveSort = useMemo( + () => + !sortQuery || columns.length === 0 + ? sortQuery + : Object.keys(sortQuery).every((id) => liveColumnIds.has(id)) + ? sortQuery + : null, + [sortQuery, columns.length, liveColumnIds] + ) + /** The payload for creating a view, and the left-hand side of the dirty check. * Carries the current layout so "Save as view" from "All" captures the widths / * order / pins the grid is rendering (they live in the table's shared metadata @@ -420,10 +449,10 @@ export function Table({ () => ({ ...(activeView?.config ?? tableData?.metadata), filter, - sort: sortQuery, - hiddenColumns, + sort: effectiveSort, + hiddenColumns: effectiveHiddenColumns, }), - [activeView, tableData?.metadata, filter, sortQuery, hiddenColumns] + [activeView, tableData?.metadata, filter, effectiveSort, effectiveHiddenColumns] ) /** @@ -433,7 +462,7 @@ export function Table({ */ const isViewDirty = activeView ? !isSameViewConfig(currentViewConfig, activeView.config) - : Boolean(filter) || Boolean(sortQuery) || hiddenColumns.length > 0 + : Boolean(filter) || Boolean(effectiveSort) || effectiveHiddenColumns.length > 0 /** Rename targets a live view rather than a snapshot, so a concurrent rename or * delete can't leave the modal editing stale data. */ @@ -442,7 +471,7 @@ export function Table({ const handleSelectView = useCallback( (viewId: string | null) => { - setTableParams({ view: viewId }) + setTableParams({ view: viewId ?? ALL_VIEW_PARAM }) }, [setTableParams] ) @@ -474,7 +503,14 @@ export function Table({ // still in flight (and vice versa). `null`/`[]` merge as explicit values, so // clearing a filter or unhiding every column still persists as a removal. updateViewMutation.mutate( - { viewId: activeView.id, configPatch: { filter, sort: sortQuery, hiddenColumns } }, + { + viewId: activeView.id, + configPatch: { + filter, + sort: effectiveSort, + hiddenColumns: effectiveHiddenColumns, + }, + }, { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save view')) } ) return @@ -511,7 +547,7 @@ export function Table({ (viewId: string) => { deleteViewMutation.mutate(viewId, { onSuccess: () => { - if (viewId === activeViewId) setTableParams({ view: null }) + if (viewId === activeViewId) setTableParams({ view: ALL_VIEW_PARAM }) }, onError: (error) => toast.error(getErrorMessage(error, 'Failed to delete view')), }) @@ -1002,7 +1038,7 @@ export function Table({ onStopRow={onStopRow} onSelectionChange={onSelectionChange} queryOptions={queryOptions} - hiddenColumns={hiddenColumns} + hiddenColumns={effectiveHiddenColumns} viewLayout={activeView?.config ?? null} viewLayoutKey={activeView?.id ?? null} onPersistLayout={activeView ? handlePersistLayout : undefined} From 20150f70140dbe1e9ccf0ddbca3a8fc55579e3c6 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 00:51:49 -0700 Subject: [PATCH 06/33] fix(tables): key-order-stable dirty check, reset layout when switching to All --- .../components/table-grid/table-grid.tsx | 16 +++++++------ .../[workspaceId]/tables/[tableId]/table.tsx | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) 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 2f4acd3e527..83cc590f54f 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 @@ -1759,20 +1759,22 @@ export function TableGrid({ // With a view active its config owns the layout; otherwise the table's own // metadata does. Switching views re-seeds unconditionally so the incoming // view's layout replaces the outgoing one. - const source = viewLayout ?? tableData?.metadata + const source = viewLayout ?? tableData?.metadata ?? null const switchedView = viewLayoutKey !== seededLayoutKeyRef.current - if (!source) return - if (!source.columnWidths && !source.columnOrder && !source.pinnedColumns && !switchedView) - return if (!metadataSeededRef.current || switchedView) { + // A switch resets even when there is nothing to seed from — a table created + // with `metadata: null` would otherwise keep showing the outgoing view's + // layout, since layout written under a view never populates table metadata. + if (!source && !switchedView) return metadataSeededRef.current = true seededLayoutKeyRef.current = viewLayoutKey - setColumnWidths(source.columnWidths ?? {}) - setColumnOrder(source.columnOrder ?? null) - setPinnedColumns(source.pinnedColumns ?? []) + setColumnWidths(source?.columnWidths ?? {}) + setColumnOrder(source?.columnOrder ?? null) + setPinnedColumns(source?.pinnedColumns ?? []) return } + if (!source) return // After first load: only re-seed `columnOrder` when the *set of columns* // changes (e.g. a workflow group adds/removes outputs server-side). Pure // reorders are left alone so an in-flight optimistic drag isn't clobbered diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index fb096c9d571..ba5d5a72d3f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -141,18 +141,33 @@ const NO_VIEWS: TableViewWire[] = [] type ViewModalState = { mode: 'create' } | { mode: 'rename'; viewId: string } | null +/** + * Order-insensitive JSON, used to compare a locally-built config against one that + * has round-tripped through Postgres. `jsonb` does not preserve object key order + * (`{status,plan}` comes back `{plan,status}`), so a plain `JSON.stringify` would + * report any multi-key filter as permanently dirty. Array order is preserved — + * it is meaningful for `columnOrder`. + */ +function stableStringify(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null' + if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]` + const entries = Object.entries(value as Record) + .filter(([, entry]) => entry !== undefined) + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(',')}}` +} + /** * Structural equality for the parts of a view config the user edits directly. * Column layout (widths/order/pinning) is excluded — it auto-saves into the * active view as the user drags, so it can never be the thing that is "unsaved". * - * Compares JSON rather than field-by-field because `filter` is an arbitrarily - * nested predicate tree; key order is stable since both sides are built by the - * same converters. + * Compares serialized form rather than field-by-field because `filter` is an + * arbitrarily nested predicate tree. */ function isSameViewConfig(a: TableViewConfig, b: TableViewConfig): boolean { const normalize = (config: TableViewConfig) => - JSON.stringify({ + stableStringify({ filter: config.filter ?? null, sort: config.sort ?? null, hiddenColumns: [...(config.hiddenColumns ?? [])].sort(), From 37f7db007480aca8d486be87075c3185d462f2ee Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 01:00:32 -0700 Subject: [PATCH 07/33] fix(tables): return 404 when patching a missing view --- apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts | 3 +++ apps/sim/lib/table/views/service.ts | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts index 50cffe69f8f..5476eef1f5b 100644 --- a/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts @@ -51,6 +51,9 @@ export const PATCH = withRouteHandler( isDefault, columns, }) + if (!view) { + return NextResponse.json({ error: 'View not found' }, { status: 404 }) + } return NextResponse.json({ success: true, data: { view } }) } catch (error) { diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts index 72c1f4adf7c..a83e3785553 100644 --- a/apps/sim/lib/table/views/service.ts +++ b/apps/sim/lib/table/views/service.ts @@ -161,7 +161,7 @@ export interface UpdateTableViewData { * overlapping partial writes — a column resize landing while a pin is in flight — * can't each replace the whole blob from their own stale snapshot. */ -export async function updateTableView(data: UpdateTableViewData): Promise { +export async function updateTableView(data: UpdateTableViewData): Promise { const patch: Partial = { updatedAt: new Date() } if (data.name !== undefined) patch.name = normalizeName(data.name) if (data.config !== undefined) patch.config = data.config @@ -193,7 +193,9 @@ export async function updateTableView(data: UpdateTableViewData): Promise Date: Sat, 25 Jul 2026 01:50:28 -0700 Subject: [PATCH 08/33] improvement(tables): order the bar filter, sort, columns and drop the hidden count --- .../resource-options/resource-options.tsx | 16 ++++++++- .../components/columns-menu/columns-menu.tsx | 4 ++- .../[workspaceId]/tables/[tableId]/table.tsx | 34 ++++++++++--------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx index ebbf38ed20f..212f216a92a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx @@ -91,6 +91,12 @@ interface ResourceOptionsProps { * widgets; primary actions belong in the header's `actions`. */ aside?: ReactNode + /** + * Mirror of {@link aside} on the other side: rendered immediately to the RIGHT + * of the filter/sort cluster and still grouped with it — e.g. the table editor's + * Columns menu, which reads as the last item in the Filter/Sort/Columns row. + */ + asideEnd?: ReactNode /** * Control pinned to the far RIGHT of the bar, opposite the filter/sort cluster — * e.g. the table editor's Save-view button. Unlike `aside` it is a real action, @@ -105,6 +111,7 @@ export const ResourceOptions = memo(function ResourceOptions({ filter, filterTags, aside, + asideEnd, trailing, }: ResourceOptionsProps) { /** @@ -119,7 +126,13 @@ export const ResourceOptions = memo(function ResourceOptions({ const popoverFilter = filter && filter.mode !== 'toggle' ? filter : null const hasContent = - search || sort || filter || aside || trailing || (filterTags && filterTags.length > 0) + search || + sort || + filter || + aside || + asideEnd || + trailing || + (filterTags && filterTags.length > 0) if (!hasContent) return null return ( @@ -187,6 +200,7 @@ export const ResourceOptions = memo(function ResourceOptions({ ) : null} {sort && (isToggleFilter || !popoverFilter) && } + {asideEnd} {trailing &&
{trailing}
} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx index d7f0d57eddc..0409ee2cf6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/columns-menu/columns-menu.tsx @@ -80,8 +80,10 @@ export const ColumnsMenu = memo(function ColumnsMenu({ return ( + {/* `active` alone signals that something is hidden — the label stays fixed + so the bar doesn't reflow as columns are toggled. */} 0} leftIcon={Columns3}> - {hiddenCount > 0 ? `Columns (${hiddenCount} hidden)` : 'Columns'} + Columns {viewsEnabled && ( - <> - - - + )} {embedded && (selection.totalRunning > 0 || selection.hasActiveDispatch) ? ( } + asideEnd={ + viewsEnabled ? ( + + ) : undefined + } trailing={ viewsEnabled && isViewDirty && userPermissions.canEdit ? ( From f085726b0aaaafbb6877ed432ae43d1d8cffdf3f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 25 Jul 2026 01:54:52 -0700 Subject: [PATCH 09/33] fix(tables): record newly created columns in the active view's order --- .../components/table-grid/table-grid.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 83cc590f54f..ad396496149 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 @@ -2078,6 +2078,28 @@ export function TableGrid({ const updateMetadataRef = useRef(updateMetadataMutation.mutate) updateMetadataRef.current = onPersistLayout ?? updateMetadataMutation.mutate + /** + * Records columns created since the layout was last saved. A new column already + * *renders* — `displayColumns` appends anything missing from `columnOrder` — but + * without this only the insert-left/right path wrote it back, so creating one via + * "+ New column", a workflow group, or an enrichment left it out of the stored + * order. With a view active this writes into that view, which is what makes a + * column added while a view is open belong to it. + * + * No-ops when the table has no explicit order (schema order governs, nothing to + * record) and when nothing is missing, so it self-heals once and stays quiet. + */ + useEffect(() => { + const order = columnOrderRef.current + if (!order || columns.length === 0) return + const known = new Set(order) + const appended = columns.map(getColumnId).filter((id) => !known.has(id)) + if (appended.length === 0) return + const nextOrder = [...order, ...appended] + setColumnOrder(nextOrder) + updateMetadataRef.current({ columnOrder: nextOrder }) + }, [columns]) + const deleteWorkflowGroupRef = useRef(deleteWorkflowGroupMutation.mutate) deleteWorkflowGroupRef.current = deleteWorkflowGroupMutation.mutate From 1ee10eb39e8074ee82aa357b9c8c8523ec44ae06 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 27 Jul 2026 19:56:19 -0700 Subject: [PATCH 10/33] fix(tables): don't write layout as a reader, clear dead sort, reset dead view id --- .../components/table-grid/table-grid.tsx | 25 ++++-- .../[workspaceId]/tables/[tableId]/table.tsx | 87 +++++++++++++------ 2 files changed, 80 insertions(+), 32 deletions(-) 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 dd44a70cfcd..d6bbdb1e78b 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 @@ -1894,12 +1894,21 @@ export function TableGrid({ const serverOrder = source.columnOrder if (serverOrder) { const localOrder = columnOrderRef.current - const serverSet = new Set(serverOrder) - const localSet = new Set(localOrder ?? []) - const setChanged = - !localOrder || serverSet.size !== localSet.size || serverOrder.some((n) => !localSet.has(n)) - if (setChanged) { + if (!localOrder) { setColumnOrder(serverOrder) + } else { + // Re-seed only when the server knows an id the local order lacks — a real + // schema change (a workflow group gained outputs). Ids present locally but + // NOT on the server are our own just-appended columns whose patch is still + // in flight: `viewLayout` gets a new identity on every save, so a refetch + // carrying the pre-append order would otherwise roll them back, and the + // append effect can't re-fire because `columns` is unchanged. Ids the + // server drops stay in the local order harmlessly — `displayColumns` + // skips any id with no matching column. + const localSet = new Set(localOrder) + if (serverOrder.some((id) => !localSet.has(id))) { + setColumnOrder(serverOrder) + } } } }, [tableData?.metadata, viewLayout, viewLayoutKey]) @@ -2229,7 +2238,11 @@ export function TableGrid({ if (appended.length === 0) return const nextOrder = [...order, ...appended] setColumnOrder(nextOrder) - updateMetadataRef.current({ columnOrder: nextOrder }) + // Render locally regardless, but only WRITE when the viewer may. Both sinks + // are write-gated, so a read-only member opening a view whose stored order + // lags the schema would otherwise fire a doomed PATCH and an error toast + // just by looking at it. + if (canEditRef.current) updateMetadataRef.current({ columnOrder: nextOrder }) }, [columns]) const deleteWorkflowGroupRef = useRef(deleteWorkflowGroupMutation.mutate) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index ec34f330fb3..89360904d59 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -263,6 +263,13 @@ export function Table({ const [{ sort: sortColumn, dir: sortDirection, view: activeViewId }, setTableParams] = useQueryStates(tableDetailParsers, tableDetailUrlKeys) + // Read-only mirrors for the resolve effect: it must know whether the user has + // already applied a filter / hidden columns without re-running when they change. + const filterRef = useRef(filter) + filterRef.current = filter + const hiddenColumnsRef = useRef(hiddenColumns) + hiddenColumnsRef.current = hiddenColumns + /** Resolved single-column sort, or `null` when no column is active. */ const sortQuery = useMemo( () => (sortColumn ? { [sortColumn]: sortDirection } : null), @@ -387,13 +394,22 @@ export function Table({ * `undefined` means "nothing seeded yet" so the first resolve still runs. */ const seededViewIdRef = useRef(undefined) - /** `keepUrlSort` leaves an explicitly deep-linked `?sort=` alone on the first - * seed — the URL is more specific than the view's stored default. */ + /** + * Applies a view's config to the live state. `keep` marks slices the user has + * already set by hand, which win over the view's stored values on the FIRST + * resolve only — a deep-linked `?sort=` is more specific than the view's default, + * and a filter typed while the views query was still in flight shouldn't be + * thrown away when it lands. Switching views later passes no `keep`, so the + * incoming view fully replaces the outgoing one. + */ const applyViewConfig = useCallback( - (config: TableViewConfig | null, keepUrlSort = false) => { - setFilter(config?.filter ?? null) - setHiddenColumns(config?.hiddenColumns ?? []) - if (keepUrlSort) return + ( + config: TableViewConfig | null, + keep?: { sort?: boolean; filter?: boolean; hiddenColumns?: boolean } + ) => { + if (!keep?.filter) setFilter(config?.filter ?? null) + if (!keep?.hiddenColumns) setHiddenColumns(config?.hiddenColumns ?? []) + if (keep?.sort) return const sortEntry = config?.sort ? Object.entries(config.sort)[0] : undefined setTableParams({ sort: sortEntry ? sortEntry[0] : null, @@ -403,6 +419,13 @@ export function Table({ [setTableParams] ) + /** What the user has already set by hand, for the first-resolve `keep`. */ + const localWork = () => ({ + sort: sortColumn !== null, + filter: filterRef.current !== null, + hiddenColumns: hiddenColumnsRef.current.length > 0, + }) + /** * Resolves the active view and seeds the local filter/sort/hidden-column state * from it. Runs only when the *selected view id* changes, never on every edit, @@ -421,7 +444,7 @@ export function Table({ if (defaultView) { seededViewIdRef.current = defaultView.id setTableParams({ view: defaultView.id }) - applyViewConfig(defaultView.config, sortColumn !== null) + applyViewConfig(defaultView.config, localWork()) return } // No view to adopt. Deliberately does NOT apply an empty config — that @@ -437,15 +460,23 @@ export function Table({ // back to "All" without touching state, for the same reason. An explicit // `?sort=` alongside `?view=` also wins over the view's stored sort. seededViewIdRef.current = activeView?.id ?? null - if (activeView) applyViewConfig(activeView.config, sortColumn !== null) + if (activeView) applyViewConfig(activeView.config, localWork()) return } - // A selected id that doesn't resolve yet must not clear anything: right after - // "Save as view" the URL names the new view before the list has refetched, and - // clearing there would wipe the very filter that was just saved. Only an - // explicit switch to "All" (`activeViewId === null`) resets. - if (activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && !activeView) return + // A selected id that doesn't resolve is one of two things. Ours — "Save as + // view" stamps `seededViewIdRef` before writing the URL, so the id can name a + // view the list hasn't refetched yet; clearing there would wipe the filter + // just saved. Or genuinely dead (deleted by someone else, stale bookmark), in + // which case leaving it applied would keep the grid narrowed under an "All" + // label, since the menu resolves the same missing view to null. + if (activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && !activeView) { + if (seededViewIdRef.current === activeViewId) return + seededViewIdRef.current = null + setTableParams({ view: ALL_VIEW_PARAM }) + applyViewConfig(null) + return + } const nextViewId = activeView?.id ?? null if (seededViewIdRef.current === nextViewId) return @@ -476,15 +507,19 @@ export function Table({ columns.length === 0 ? hiddenColumns : hiddenColumns.filter((id) => liveColumnIds.has(id)), [columns.length, hiddenColumns, liveColumnIds] ) - const effectiveSort = useMemo( - () => - !sortQuery || columns.length === 0 - ? sortQuery - : Object.keys(sortQuery).every((id) => liveColumnIds.has(id)) - ? sortQuery - : null, - [sortQuery, columns.length, liveColumnIds] - ) + + /** + * Drops a sort whose column was deleted by clearing the URL, rather than masking + * it in a derived value: `queryOptions` feeds the query that produces `columns`, + * so a pruned sort can't flow back into it without a cycle. Clearing keeps one + * source of truth, so the rows query, the dirty check, and the Save patch can't + * disagree about whether a sort is active. + */ + useEffect(() => { + if (!sortColumn || columns.length === 0) return + if (liveColumnIds.has(sortColumn)) return + setTableParams({ sort: null, dir: null }) + }, [sortColumn, columns.length, liveColumnIds, setTableParams]) /** The payload for creating a view, and the left-hand side of the dirty check. * Carries the current layout so "Save as view" from "All" captures the widths / @@ -495,10 +530,10 @@ export function Table({ () => ({ ...(activeView?.config ?? tableData?.metadata), filter, - sort: effectiveSort, + sort: sortQuery, hiddenColumns: effectiveHiddenColumns, }), - [activeView, tableData?.metadata, filter, effectiveSort, effectiveHiddenColumns] + [activeView, tableData?.metadata, filter, sortQuery, effectiveHiddenColumns] ) /** @@ -508,7 +543,7 @@ export function Table({ */ const isViewDirty = activeView ? !isSameViewConfig(currentViewConfig, activeView.config) - : Boolean(filter) || Boolean(effectiveSort) || effectiveHiddenColumns.length > 0 + : Boolean(filter) || Boolean(sortQuery) || effectiveHiddenColumns.length > 0 /** Rename targets a live view rather than a snapshot, so a concurrent rename or * delete can't leave the modal editing stale data. */ @@ -553,7 +588,7 @@ export function Table({ viewId: activeView.id, configPatch: { filter, - sort: effectiveSort, + sort: sortQuery, hiddenColumns: effectiveHiddenColumns, }, }, From 37627a183695440b0014d43c5e1216f290b922f8 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 27 Jul 2026 20:07:36 -0700 Subject: [PATCH 11/33] feat(tables): add New view to the views menu, starting from All --- .../save-view-modal/save-view-modal.tsx | 6 ++-- .../components/views-menu/views-menu.tsx | 19 ++++++++++- .../[workspaceId]/tables/[tableId]/table.tsx | 34 ++++++++++++++++--- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx index b8f1dfcc742..a25de7f2716 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/save-view-modal/save-view-modal.tsx @@ -14,7 +14,9 @@ interface SaveViewModalProps { onOpenChange: (open: boolean) => void /** Pre-filled when renaming an existing view; empty when saving a new one. */ initialName?: string - mode: 'create' | 'rename' + /** `new` starts blank and is configured after; `create` captures what is + * already applied; `rename` retitles an existing view. */ + mode: 'new' | 'create' | 'rename' onSubmit: (name: string) => void isSubmitting: boolean } @@ -41,7 +43,7 @@ export function SaveViewModal({ } const trimmed = name.trim() - const title = mode === 'create' ? 'Save as view' : 'Rename view' + const title = mode === 'new' ? 'New view' : mode === 'create' ? 'Save as view' : 'Rename view' const handleSubmit = () => { if (!trimmed || isSubmitting) return diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx index dc3cd443aea..08a3643653d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx @@ -13,7 +13,7 @@ import { PopoverItem, PopoverSection, } from '@sim/emcn' -import { Check, Pencil, Trash } from '@sim/emcn/icons' +import { Check, Pencil, Plus, Trash } from '@sim/emcn/icons' import type { TableViewWire } from '@/lib/api/contracts/tables' /** Label for the built-in unfiltered state. Not a stored row — `null` view id. */ @@ -34,6 +34,8 @@ interface ViewsMenuProps { onSelect: (viewId: string | null) => void onRename: (viewId: string) => void onDelete: (viewId: string) => void + /** Starts a blank view — named first, configured after. */ + onNewView: () => void /** Read-only members can switch views but not modify them. */ canEdit: boolean } @@ -51,6 +53,7 @@ export const ViewsMenu = memo(function ViewsMenu({ onSelect, onRename, onDelete, + onNewView, canEdit, }: ViewsMenuProps) { const [open, setOpen] = useState(false) @@ -160,6 +163,20 @@ export const ViewsMenu = memo(function ViewsMenu({ /> ))} + {canEdit && ( + <> +
+ runAndClose(onNewView)} + className='h-7 items-center gap-1.5 px-1.5 py-0 text-xs' + > + + + + New view + + + )} ) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 89360904d59..69814b95693 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -156,7 +156,12 @@ function slideoutReducer(_state: SlideoutState, action: SlideoutAction): Slideou /** Stable identity so a loading/disabled views query doesn't remint `[]` each render. */ const NO_VIEWS: TableViewWire[] = [] -type ViewModalState = { mode: 'create' } | { mode: 'rename'; viewId: string } | null +/** `blank` starts the view from "All" (no filter/sort/hidden) so it is configured + * after naming, rather than capturing whatever is currently applied. */ +type ViewModalState = + | { mode: 'create'; blank?: boolean } + | { mode: 'rename'; viewId: string } + | null /** * Order-insensitive JSON, used to compare a locally-built config against one that @@ -561,6 +566,10 @@ export function Table({ setViewModal({ mode: 'rename', viewId }) }, []) + const handleNewView = useCallback(() => { + setViewModal({ mode: 'create', blank: true }) + }, []) + /** Column order/width/pinning auto-saves into the active view as the user drags, * which is why `isSameViewConfig` excludes layout from the dirty check. Sent as * a `configPatch` so the server merges it — two overlapping layout writes must @@ -610,14 +619,30 @@ export function Table({ ) return } + // "New view" starts from All and is configured afterwards; "Save as view" + // captures what is already applied. Both keep the current column layout so + // creating a view never visually resets the grid. + const blank = viewModal?.blank === true + const config: TableViewConfig = blank + ? { + ...(activeView?.config ?? tableData?.metadata), + filter: null, + sort: null, + hiddenColumns: [], + } + : currentViewConfig createViewMutation.mutate( - { name, config: currentViewConfig }, + { name, config }, { onSuccess: (view) => { setViewModal(null) - // Mark as seeded before selecting so the resolve effect doesn't re-apply the config. + // Stamp before selecting so the resolve effect treats this as already + // seeded — it can't tell a just-created view from a dead id otherwise. seededViewIdRef.current = view.id setTableParams({ view: view.id }) + // Which means the blank config must be applied here; nuqs batches this + // sort write with the `view` write above into one URL update. + if (blank) applyViewConfig(view.config) }, onError: (error) => toast.error(getErrorMessage(error, 'Failed to create view')), } @@ -1126,6 +1151,7 @@ export function Table({ onSelect={handleSelectView} onRename={handleRenameView} onDelete={handleDeleteView} + onNewView={handleNewView} canEdit={userPermissions.canEdit} /> )} @@ -1168,7 +1194,7 @@ export function Table({ !open && setViewModal(null)} - mode={viewModal?.mode ?? 'create'} + mode={viewModal?.mode === 'rename' ? 'rename' : viewModal?.blank ? 'new' : 'create'} initialName={renamingView?.name ?? ''} onSubmit={handleSubmitViewName} isSubmitting={createViewMutation.isPending || updateViewMutation.isPending} From 86c7d26f443485215a374bbd2eeddb99952e9654 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 09:29:37 -0700 Subject: [PATCH 12/33] chore(api): bump route-count baseline to 981 after staging merge --- scripts/check-api-validation-contracts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 0751de73108..1e073231776 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: 979, - zodRoutes: 979, + totalRoutes: 981, + zodRoutes: 981, nonZodRoutes: 0, } as const From da8a17aa3042850827100f01f437f9c7d6010914 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 09:34:18 -0700 Subject: [PATCH 13/33] fix(tables): guard default demotion, use the applied filter for dirty/save --- .../[workspaceId]/tables/[tableId]/table.tsx | 14 ++++++++++---- apps/sim/lib/table/views/service.ts | 11 +++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index c9eb7517b3d..0028a7d9335 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -529,11 +529,11 @@ export function Table({ const currentViewConfig = useMemo( () => ({ ...(activeView?.config ?? tableData?.metadata), - filter, + filter: effectiveFilter, sort: sortQuery, hiddenColumns: effectiveHiddenColumns, }), - [activeView, tableData?.metadata, filter, sortQuery, effectiveHiddenColumns] + [activeView, tableData?.metadata, effectiveFilter, sortQuery, effectiveHiddenColumns] ) /** @@ -543,7 +543,7 @@ export function Table({ */ const isViewDirty = activeView ? !isSameViewConfig(currentViewConfig, activeView.config) - : Boolean(filter) || Boolean(sortQuery) || effectiveHiddenColumns.length > 0 + : Boolean(effectiveFilter) || Boolean(sortQuery) || effectiveHiddenColumns.length > 0 /** Rename targets a live view rather than a snapshot, so a concurrent rename or * delete can't leave the modal editing stale data. */ @@ -570,8 +570,14 @@ export function Table({ * a `configPatch` so the server merges it — two overlapping layout writes must * not each replace the whole blob from their own snapshot. With no view active * the grid keeps writing the table's shared metadata. */ + /** Last layout the grid reported, patch by patch. View layout writes have no + * optimistic cache update, so `activeView.config` lags an in-flight resize — + * this is what a blank "New view" copies so the grid can't snap back. */ + const liveLayoutRef = useRef({}) + const handlePersistLayout = useCallback( (patch: TableMetadata) => { + liveLayoutRef.current = { ...liveLayoutRef.current, ...patch } if (!activeView) return updateViewMutation.mutate( { viewId: activeView.id, configPatch: patch }, @@ -591,7 +597,7 @@ export function Table({ { viewId: activeView.id, configPatch: { - filter, + filter: effectiveFilter, sort: sortQuery, hiddenColumns: effectiveHiddenColumns, }, diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts index a83e3785553..1b575bb618a 100644 --- a/apps/sim/lib/table/views/service.ts +++ b/apps/sim/lib/table/views/service.ts @@ -171,6 +171,17 @@ export async function updateTableView(data: UpdateTableViewData): Promise { + // Confirm the target exists BEFORE demoting. The demotion has to run first — + // the partial unique index rejects a second default — but on a PATCH naming a + // missing view the target update matches nothing, so without this the demote + // would still commit and silently clear the table's real default. + const [existing] = await tx + .select({ id: tableViews.id }) + .from(tableViews) + .where(and(eq(tableViews.id, data.viewId), eq(tableViews.tableId, data.tableId))) + .limit(1) + if (!existing) return null + if (data.isDefault === true) { await tx .update(tableViews) From 6c7d1447742de493a03e2cee4ff4e41f6fad967c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 12:02:50 -0700 Subject: [PATCH 14/33] fix(tables): clear state when the active view is deleted externally --- .../[workspaceId]/tables/[tableId]/table.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 0028a7d9335..22f0f396153 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -394,6 +394,14 @@ export function Table({ * `undefined` means "nothing seeded yet" so the first resolve still runs. */ const seededViewIdRef = useRef(undefined) + /** + * A view this client just created, held only until the list refetch carries it. + * Distinct from `seededViewIdRef`, which is stamped on EVERY selection — reusing + * that for the create race also matched a view that had been selected normally + * and then deleted, so the delete never cleaned up. + */ + const pendingCreatedViewIdRef = useRef(null) + /** * Applies a view's config to the live state. `keep` marks slices the user has * already set by hand, which win over the view's stored values on the FIRST @@ -464,14 +472,18 @@ export function Table({ return } - // A selected id that doesn't resolve is one of two things. Ours — "Save as - // view" stamps `seededViewIdRef` before writing the URL, so the id can name a - // view the list hasn't refetched yet; clearing there would wipe the filter - // just saved. Or genuinely dead (deleted by someone else, stale bookmark), in - // which case leaving it applied would keep the grid narrowed under an "All" + // The id resolved, so any create race for it is over. + if (activeView && pendingCreatedViewIdRef.current === activeView.id) { + pendingCreatedViewIdRef.current = null + } + + // A selected id that doesn't resolve is one of two things. Ours — creation + // writes the URL before the list refetches, and clearing there would wipe the + // config just saved. Or genuinely dead (deleted by someone else, stale + // bookmark), where leaving it applied keeps the grid narrowed under an "All" // label, since the menu resolves the same missing view to null. if (activeViewId !== null && activeViewId !== ALL_VIEW_PARAM && !activeView) { - if (seededViewIdRef.current === activeViewId) return + if (pendingCreatedViewIdRef.current === activeViewId) return seededViewIdRef.current = null setTableParams({ view: ALL_VIEW_PARAM }) applyViewConfig(null) @@ -640,6 +652,7 @@ export function Table({ // Stamp before selecting so the resolve effect treats this as already // seeded — it can't tell a just-created view from a dead id otherwise. seededViewIdRef.current = view.id + pendingCreatedViewIdRef.current = view.id setTableParams({ view: view.id }) // Which means the blank config must be applied here; nuqs batches this // sort write with the `view` write above into one URL update. From 1aae25b7600ba337ef7ec9866cf7bdb92857ecab Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 13:01:52 -0700 Subject: [PATCH 15/33] revert(tables): drop the ineffective rows-query gate for view resolution --- .../tables/[tableId]/hooks/use-table.ts | 17 ++--------------- .../[workspaceId]/tables/[tableId]/table.tsx | 6 ------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts index b6baace288c..9fcf4dd7db5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts @@ -32,14 +32,6 @@ interface UseTableParams { workspaceId: string tableId: string queryOptions: QueryOptions - /** - * Holds the rows query until the caller's filter/sort are settled. The wrapper - * resolves the active view asynchronously, so without this the first fetch runs - * against an empty filter and the grid paints the unfiltered set before - * refetching — a visible flash of the wrong rows on every load that adopts a - * default view or opens a `?table-view=` link. - */ - rowsEnabled?: boolean } interface FetchNextPageResult { @@ -96,12 +88,7 @@ export interface UseTableReturn { * stays in the `Table` component — moving it here would push every keystroke * through this hook's return value and re-render everything. */ -export function useTable({ - workspaceId, - tableId, - queryOptions, - rowsEnabled = true, -}: UseTableParams): UseTableReturn { +export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams): UseTableReturn { const queryClient = useQueryClient() const { data: tableData, isLoading: isLoadingTable } = useTableQuery(workspaceId, tableId) @@ -128,7 +115,7 @@ export function useTable({ pageSize: TABLE_LIMITS.MAX_QUERY_LIMIT, filter, sort: queryOptions.sort, - enabled: Boolean(workspaceId && tableId) && rowsEnabled, + enabled: Boolean(workspaceId && tableId), }) const rows = useMemo( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index e3a47dd8936..45be1352f67 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -358,8 +358,6 @@ export function Table({ ((previousName: string, newName: string) => void) | null >(null) - // Declared before `useTable`: the rows query is gated on `viewsLoaded`, since a - // view owns the filter/sort the query runs with. const { data: views = NO_VIEWS, isSuccess: viewsLoaded } = useTableViews({ workspaceId, tableId, @@ -381,10 +379,6 @@ export function Table({ workspaceId, tableId, queryOptions, - // Without this the first fetch runs against an empty filter and the grid - // paints the unfiltered set before refetching. Gates only the first pass — - // `viewsLoaded` stays true after, so later filter edits fetch immediately. - rowsEnabled: !viewsEnabled || viewsLoaded, }) const createViewMutation = useCreateTableView({ workspaceId, tableId }) const updateViewMutation = useUpdateTableView({ workspaceId, tableId }) From acec9094e5dc791c910cb7c9682000530fc74f85 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 13:24:36 -0700 Subject: [PATCH 16/33] feat(tables): enable saved views in the embedded mothership table --- .../resource-content/resource-content.tsx | 14 +++++++++++++- .../mothership-view/mothership-view.tsx | 4 ++++ .../app/workspace/[workspaceId]/home/home.tsx | 5 ++++- .../app/workspace/[workspaceId]/home/page.tsx | 18 +++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 95a79eee750..6fda3287cde 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -82,6 +82,9 @@ interface ResourceContentProps { isAgentResponding?: boolean genericResourceData?: GenericResourceData previewContextKey?: string + /** Resolved server-side by the home page — the embedded table can't read + * AppConfig itself, so the flag is threaded down rather than looked up. */ + tableViewsEnabled?: boolean onNotFound?: (resourceId: string) => void } @@ -144,6 +147,7 @@ export const ResourceContent = memo(function ResourceContent({ isAgentResponding, genericResourceData, previewContextKey, + tableViewsEnabled, onNotFound, }: ResourceContentProps) { const streamFileName = previewSession?.fileName || 'file.md' @@ -213,7 +217,15 @@ export const ResourceContent = memo(function ResourceContent({ switch (resource.type) { case 'table': - return
+ return ( +
+ ) case 'file': return ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index 57f133daf14..cdfc0e9e6ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -54,6 +54,8 @@ interface MothershipViewProps { previewSession?: FilePreviewSession | null isAgentResponding?: boolean genericResourceData?: GenericResourceData + /** Resolved server-side by the home page; forwarded to the embedded table. */ + tableViewsEnabled?: boolean } export const MothershipView = memo( @@ -68,6 +70,7 @@ export const MothershipView = memo( previewSession, isAgentResponding, genericResourceData, + tableViewsEnabled, }: MothershipViewProps, ref ) { @@ -141,6 +144,7 @@ export const MothershipView = memo( isAgentResponding={isAgentResponding} genericResourceData={active.type === 'generic' ? genericResourceData : undefined} previewContextKey={chatId} + tableViewsEnabled={tableViewsEnabled} onNotFound={(resourceId) => removeResource('log', resourceId)} /> ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 9776acdaf7f..d781b2d51a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -76,9 +76,11 @@ interface HomeProps { chatId?: string userName?: string userId?: string + /** Resolved server-side by the page — the embedded table can't reach AppConfig. */ + tableViewsEnabled?: boolean } -export function Home({ chatId, userName, userId }: HomeProps) { +export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps) { useOAuthReturnRouter() const { workspaceId } = useParams<{ workspaceId: string }>() const router = useRouter() @@ -539,6 +541,7 @@ export function Home({ chatId, userName, userId }: HomeProps) { previewSession={previewSession} isAgentResponding={isSending} genericResourceData={genericResourceData ?? undefined} + tableViewsEnabled={tableViewsEnabled} className={skipResourceTransition ? '!transition-none' : undefined} /> diff --git a/apps/sim/app/workspace/[workspaceId]/home/page.tsx b/apps/sim/app/workspace/[workspaceId]/home/page.tsx index 13595d65398..45b72e62a69 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/page.tsx @@ -2,6 +2,8 @@ import { Suspense } from 'react' import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' import { getSession } from '@/lib/auth' +import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { prefetchHomeLists } from '@/app/workspace/[workspaceId]/home/prefetch' import { Home } from './home' @@ -18,12 +20,26 @@ export default async function HomePage({ params }: { params: Promise<{ workspace const listsPrefetch = prefetchHomeLists(queryClient, workspaceId) const session = await getSession() + const userId = session?.user?.id + // Resolved here for the same reason the table page does it: the flag's gating + // lives in AppConfig, which has no client counterpart, and the embedded table is + // a client component. Keyed on the workspace's host organization, matching the + // table page so both surfaces gate identically. Both reads are request-memoized. + const host = userId ? await getWorkspaceHostContextForViewer(workspaceId, userId) : null + const tableViewsEnabled = await isFeatureEnabled('table-views', { + userId, + orgId: host?.hostOrganizationId ?? undefined, + }) await listsPrefetch return ( }> - + ) From 5496dee3d62e7814ca48ca9a29944e4ee4d46b7a Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 14:00:04 -0700 Subject: [PATCH 17/33] fix(tables): resolve the views flag on the chat route too --- .../[workspaceId]/chat/[chatId]/page.tsx | 8 ++++-- .../app/workspace/[workspaceId]/home/page.tsx | 13 ++-------- .../home/resolve-table-views-flag.ts | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/resolve-table-views-flag.ts diff --git a/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx index 9dacd8d8cde..2c56d1d9837 100644 --- a/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx @@ -3,6 +3,7 @@ import type { Metadata } from 'next' import { getSession } from '@/lib/auth' import { Home } from '@/app/workspace/[workspaceId]/home/home' import { HomeFallback } from '@/app/workspace/[workspaceId]/home/home-fallback' +import { resolveTableViewsEnabled } from '@/app/workspace/[workspaceId]/home/resolve-table-views-flag' export const metadata: Metadata = { title: 'Chat', @@ -16,14 +17,17 @@ interface ChatPageProps { } export default async function ChatPage({ params }: ChatPageProps) { - const [{ chatId }, session] = await Promise.all([params, getSession()]) + const [{ workspaceId, chatId }, session] = await Promise.all([params, getSession()]) + const userId = session?.user?.id + const tableViewsEnabled = await resolveTableViewsEnabled(workspaceId, userId) return ( }> ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/page.tsx b/apps/sim/app/workspace/[workspaceId]/home/page.tsx index 45b72e62a69..a1e1febf0fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/page.tsx @@ -2,10 +2,9 @@ import { Suspense } from 'react' import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' import { getSession } from '@/lib/auth' -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' -import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { prefetchHomeLists } from '@/app/workspace/[workspaceId]/home/prefetch' +import { resolveTableViewsEnabled } from '@/app/workspace/[workspaceId]/home/resolve-table-views-flag' import { Home } from './home' import { HomeFallback } from './home-fallback' @@ -21,15 +20,7 @@ export default async function HomePage({ params }: { params: Promise<{ workspace const session = await getSession() const userId = session?.user?.id - // Resolved here for the same reason the table page does it: the flag's gating - // lives in AppConfig, which has no client counterpart, and the embedded table is - // a client component. Keyed on the workspace's host organization, matching the - // table page so both surfaces gate identically. Both reads are request-memoized. - const host = userId ? await getWorkspaceHostContextForViewer(workspaceId, userId) : null - const tableViewsEnabled = await isFeatureEnabled('table-views', { - userId, - orgId: host?.hostOrganizationId ?? undefined, - }) + const tableViewsEnabled = await resolveTableViewsEnabled(workspaceId, userId) await listsPrefetch return ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/resolve-table-views-flag.ts b/apps/sim/app/workspace/[workspaceId]/home/resolve-table-views-flag.ts new file mode 100644 index 00000000000..22ead46a47c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/resolve-table-views-flag.ts @@ -0,0 +1,26 @@ +import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context' + +/** + * Resolves `table-views` for the mothership panel's embedded table. + * + * Lives here rather than inline because BOTH routes that render `` need it + * — `home/page.tsx` and `chat/[chatId]/page.tsx` — and the flag can only be read + * server-side (its gating is in AppConfig, which has no client counterpart). + * Resolving it in one place is what keeps the two routes from drifting; the chat + * route missing it is precisely why views didn't appear in the panel. + * + * Keyed on the workspace's HOST organization, matching the table page, so the + * same workspace gates identically wherever its tables are rendered. Both this + * and `getSession` are request-memoized, so callers pay no extra round-trip. + */ +export async function resolveTableViewsEnabled( + workspaceId: string, + userId: string | undefined +): Promise { + const host = userId ? await getWorkspaceHostContextForViewer(workspaceId, userId) : null + return isFeatureEnabled('table-views', { + userId, + orgId: host?.hostOrganizationId ?? undefined, + }) +} From 20f7df65f41aab25a342a33bdde081c0158d4533 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 14:03:57 -0700 Subject: [PATCH 18/33] improvement(tables): right-align the embedded run/stop control --- .../[workspaceId]/tables/[tableId]/table.tsx | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 45be1352f67..29a19830793 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -1149,6 +1149,33 @@ export function Table({ [filterOpen, effectiveFilter, handleToggleFilter] ) + const runStatus = + embedded && (selection.totalRunning > 0 || selection.hasActiveDispatch) ? ( + + ) : null + + const saveViewChip = + viewsEnabled && isViewDirty && userPermissions.canEdit ? ( + + {activeView ? 'Save' : 'Save as view'} + + ) : null + + /** Right-aligned slot. Left `undefined` when both are absent so the options bar + * doesn't render an empty flex row — a fragment would always read as truthy. */ + const optionsTrailing = + runStatus || saveViewChip ? ( + <> + {runStatus} + {saveViewChip} + + ) : undefined + return ( {!embedded && ( @@ -1182,33 +1209,23 @@ export function Table({ /> )} {/* Sort + filter render in both modes. In embedded (mothership) mode there's no - Resource.Header, so the run/stop control rides in the options bar's `aside` - slot, just left of filter/sort. */} + Resource.Header, so the run/stop control rides in the options bar — pinned + right, opposite the menu cluster, next to Save. */} - {viewsEnabled && ( - - )} - {embedded && (selection.totalRunning > 0 || selection.hasActiveDispatch) ? ( - - ) : null} - + viewsEnabled ? ( + + ) : undefined } asideEnd={ viewsEnabled ? ( @@ -1220,13 +1237,7 @@ export function Table({ /> ) : undefined } - trailing={ - viewsEnabled && isViewDirty && userPermissions.canEdit ? ( - - {activeView ? 'Save' : 'Save as view'} - - ) : undefined - } + trailing={optionsTrailing} /> {filterOpen && ( Date: Tue, 28 Jul 2026 14:08:18 -0700 Subject: [PATCH 19/33] fix(tables): live layout on new views, prune stored config, order cache writes --- .../[workspaceId]/tables/[tableId]/table.tsx | 30 +++++++++++++++++-- apps/sim/hooks/queries/tables.ts | 9 +++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 29a19830793..d327907c76d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -555,13 +555,33 @@ export function Table({ [activeView, tableData?.metadata, effectiveFilter, sortQuery, effectiveHiddenColumns] ) + /** + * The active view's stored config, pruned against the live columns exactly as + * the local state is. The server prunes on read, but the cached copy is not + * re-pruned when the schema changes here — so without this, deleting a hidden or + * sorted column makes the two sides disagree and lights Save with no user edit. + */ + const storedViewConfig = useMemo(() => { + if (!activeView) return null + const stored = activeView.config + if (columns.length === 0) return stored + return { + ...stored, + hiddenColumns: (stored.hiddenColumns ?? []).filter((id) => liveColumnIds.has(id)), + sort: + stored.sort && Object.keys(stored.sort).every((id) => liveColumnIds.has(id)) + ? stored.sort + : null, + } + }, [activeView, columns.length, liveColumnIds]) + /** * Whether the live state diverges from what the active view stores (or, on * "All", whether anything is applied at all). Drives the Save button — it is * the only affordance that persists, so ad-hoc exploration stays throwaway. */ - const isViewDirty = activeView - ? !isSameViewConfig(currentViewConfig, activeView.config) + const isViewDirty = storedViewConfig + ? !isSameViewConfig(currentViewConfig, storedViewConfig) : Boolean(effectiveFilter) || Boolean(sortQuery) || effectiveHiddenColumns.length > 0 /** Rename targets a live view rather than a snapshot, so a concurrent rename or @@ -648,7 +668,13 @@ export function Table({ const blank = viewModal?.blank === true const config: TableViewConfig = blank ? { + // `liveLayoutRef` last, so a resize/reorder/pin still in flight wins over + // the cached copy — otherwise the new view stores the pre-drag layout and + // the grid snaps back to it on selection. (With no active view the grid + // writes table metadata directly, which `useUpdateTableMetadata` updates + // optimistically, so that branch is already current.) ...(activeView?.config ?? tableData?.metadata), + ...liveLayoutRef.current, filter: null, sort: null, hiddenColumns: [], diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 74d6718a823..8b8c78fdaa0 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -1479,7 +1479,14 @@ export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext) // so `isViewDirty` re-reads true and the Save chip flashes back after a save. onSuccess: (view) => { queryClient.setQueryData(tableKeys.views(tableId), (prev) => - prev?.map((existing) => (existing.id === view.id ? view : existing)) + prev?.map((existing) => { + if (existing.id !== view.id) return existing + // Layout auto-saves and an explicit Save fire concurrently, and their + // responses can arrive out of order. The DB merge is authoritative, so + // only let a row at least as new as the cached one win — otherwise a + // slower response rewinds the cache until the refetch lands. + return new Date(view.updatedAt) >= new Date(existing.updatedAt) ? view : existing + }) ) }, onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }), From 987bbc2a6a1af8cd2a887f51fc2d0401eb3e9655 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 14:26:15 -0700 Subject: [PATCH 20/33] fix(tables): live layout on save-as-view, reset it per view, ignore inherited param when embedded --- .../[workspaceId]/tables/[tableId]/table.tsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index d327907c76d..1f11b4d2b3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -447,7 +447,11 @@ export function Table({ if (!viewsEnabled || !viewsLoaded) return if (seededViewIdRef.current === undefined) { - if (activeViewId === null) { + // Embedded tables bind these parsers to the HOST page's URL, which the + // mothership panel keeps across resource switches. A param left by the + // previously-open table must not decide this one's initial view, so the + // first resolve ignores it and lets this table pick its own default. + if (activeViewId === null || embedded) { const defaultView = views.find((view) => view.isDefault) if (defaultView) { seededViewIdRef.current = defaultView.id @@ -458,6 +462,7 @@ export function Table({ // No view to adopt. Deliberately does NOT apply an empty config — that // would clear a deep-linked `?sort=` on mount. seededViewIdRef.current = null + if (embedded && activeViewId !== null) setTableParams({ view: ALL_VIEW_PARAM }) return } if (activeViewId === ALL_VIEW_PARAM) { @@ -507,6 +512,7 @@ export function Table({ views, activeView, activeViewId, + embedded, sortColumn, applyViewConfig, setTableParams, @@ -545,9 +551,26 @@ export function Table({ * order / pins the grid is rendering (they live in the table's shared metadata * until a view owns them) instead of creating a layout-less view that then * resets the grid. Updates never send this — they send a merge patch. */ + /** Last layout the grid reported, patch by patch. View layout writes have no + * optimistic cache update, so `activeView.config` lags an in-flight resize — + * this is what a new view copies so the grid can't snap back. + * + * Declared above the consumers that read it during render, and reset on every + * view change: it holds patches for ONE view, so carrying them across a switch + * would let a new view inherit the previous view's widths/order/pins. */ + const liveLayoutRef = useRef({}) + + useEffect(() => { + liveLayoutRef.current = {} + }, [activeView?.id]) + const currentViewConfig = useMemo( () => ({ + // `liveLayoutRef` after the cached config for the same reason the blank-view + // payload does it: a resize/reorder/pin still in flight would otherwise be + // captured at its pre-drag value and snap back when the view is selected. ...(activeView?.config ?? tableData?.metadata), + ...liveLayoutRef.current, filter: effectiveFilter, sort: sortQuery, hiddenColumns: effectiveHiddenColumns, @@ -609,11 +632,6 @@ export function Table({ * a `configPatch` so the server merges it — two overlapping layout writes must * not each replace the whole blob from their own snapshot. With no view active * the grid keeps writing the table's shared metadata. */ - /** Last layout the grid reported, patch by patch. View layout writes have no - * optimistic cache update, so `activeView.config` lags an in-flight resize — - * this is what a blank "New view" copies so the grid can't snap back. */ - const liveLayoutRef = useRef({}) - const handlePersistLayout = useCallback( (patch: TableMetadata) => { liveLayoutRef.current = { ...liveLayoutRef.current, ...patch } From 4f51aade57219e6a0022d0f6d28e1a89eee09d67 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 14:32:03 -0700 Subject: [PATCH 21/33] fix(tables): route undo/redo column-layout writes to the active view --- .../components/table-grid/table-grid.tsx | 1 + apps/sim/hooks/use-table-undo.ts | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) 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 d6bbdb1e78b..856150bbf39 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 @@ -729,6 +729,7 @@ export function TableGrid({ onPinnedColumnsChange: handlePinnedColumnsChange, getPinnedColumns, getColumnWidths, + onPersistLayout, }) const undoRef = useRef(undo) undoRef.current = undo diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index 6d7346d6282..5f29f847d10 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -2,7 +2,12 @@ import { useCallback, useEffect, useRef } from 'react' import { toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { TABLE_LIMITS } from '@/lib/table/constants' -import { TABLE_LOCK_FLAGS, type TableLockKind, type TableLocks } from '@/lib/table/types' +import { + TABLE_LOCK_FLAGS, + type TableLockKind, + type TableLocks, + type TableMetadata, +} from '@/lib/table/types' import { useAddTableColumn, useBatchCreateTableRows, @@ -98,6 +103,13 @@ interface UseTableUndoProps { onPinnedColumnsChange?: (pinned: string[]) => void getPinnedColumns?: () => string[] getColumnWidths?: () => Record + /** + * Sink for column-layout writes (order, widths, pinning). Defaults to the + * table's shared metadata; with a saved view active the caller passes the + * view's sink instead, so undoing a reorder unwinds it where the original + * reorder was stored rather than silently rewriting the "All" layout. + */ + onPersistLayout?: (patch: TableMetadata) => void } export function useTableUndo({ @@ -110,6 +122,7 @@ export function useTableUndo({ onPinnedColumnsChange, getPinnedColumns, getColumnWidths, + onPersistLayout, }: UseTableUndoProps) { const push = useTableUndoStore((s) => s.push) const popUndo = useTableUndoStore((s) => s.popUndo) @@ -131,6 +144,9 @@ export function useTableUndo({ const renameTableMutation = useRenameTable(workspaceId) const updateMetadataMutation = useUpdateTableMetadata({ workspaceId, tableId }) + const persistLayoutRef = useRef(onPersistLayout ?? updateMetadataMutation.mutate) + persistLayoutRef.current = onPersistLayout ?? updateMetadataMutation.mutate + const onColumnOrderChangeRef = useRef(onColumnOrderChange) onColumnOrderChangeRef.current = onColumnOrderChange const onColumnRenameRef = useRef(onColumnRename) @@ -293,7 +309,7 @@ export function useTableUndo({ if (direction === 'undo') { deleteColumnMutation.mutate(colKey, { onSuccess: () => { - const metadata: Record = {} + const metadata: TableMetadata = {} const currentWidths = getColumnWidthsRef.current?.() ?? {} if (colKey in currentWidths) { const { [colKey]: _, ...rest } = currentWidths @@ -307,7 +323,7 @@ export function useTableUndo({ metadata.pinnedColumns = newPinned } if (Object.keys(metadata).length > 0) { - updateMetadataMutation.mutate(metadata) + persistLayoutRef.current(metadata) } }, }) @@ -368,7 +384,7 @@ export function useTableUndo({ } })() } - const metadata: Record = {} + const metadata: TableMetadata = {} if (action.previousOrder) { onColumnOrderChangeRef.current?.(action.previousOrder) metadata.columnOrder = action.previousOrder @@ -399,7 +415,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - updateMetadataMutation.mutate(metadata) + persistLayoutRef.current(metadata) } }, } @@ -407,7 +423,7 @@ export function useTableUndo({ } else { deleteColumnMutation.mutate(colKey, { onSuccess: () => { - const metadata: Record = {} + const metadata: TableMetadata = {} if (action.previousOrder) { const newOrder = action.previousOrder.filter((n) => n !== colKey) onColumnOrderChangeRef.current?.(newOrder) @@ -428,7 +444,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - updateMetadataMutation.mutate(metadata) + persistLayoutRef.current(metadata) } }, }) @@ -491,7 +507,7 @@ export function useTableUndo({ ] } onColumnOrderChangeRef.current?.(order) - updateMetadataMutation.mutate({ columnOrder: order }) + persistLayoutRef.current({ columnOrder: order }) break } } From c584cde7d1b755980b7a8bd496b0e9ea5690ec46 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 15:24:15 -0700 Subject: [PATCH 22/33] fix(tables): scope layout undo to the view that recorded it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout is view-owned, but UndoEntry carried no owner, so the persistence sink — rebound every render to the active view — decided the target at undo time rather than at record time. Reordering in view A then undoing from view B wrote A's column order into B and left A unchanged. Entries are now stamped with the active view id, and the three layout-bearing action types (create-column, delete-column, reorder-columns) are pruned from both stacks when the active view changes. Row and schema actions are table-scoped and survive the switch untouched. Inert when views are disabled: the id is always null, so nothing is ever pruned. --- .../components/table-grid/table-grid.tsx | 1 + apps/sim/hooks/use-table-undo.test.ts | 2 + apps/sim/hooks/use-table-undo.ts | 17 ++++- apps/sim/stores/table/store.test.ts | 75 +++++++++++++++++++ apps/sim/stores/table/store.ts | 21 +++++- apps/sim/stores/table/types.ts | 24 +++++- 6 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 apps/sim/stores/table/store.test.ts 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 856150bbf39..e8316fd7950 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 @@ -730,6 +730,7 @@ export function TableGrid({ getPinnedColumns, getColumnWidths, onPersistLayout, + activeViewId: viewLayoutKey, }) const undoRef = useRef(undo) undoRef.current = undo diff --git a/apps/sim/hooks/use-table-undo.test.ts b/apps/sim/hooks/use-table-undo.test.ts index 7bb61cb90e7..0456f76c087 100644 --- a/apps/sim/hooks/use-table-undo.test.ts +++ b/apps/sim/hooks/use-table-undo.test.ts @@ -41,6 +41,7 @@ const mockPush = vi.fn() const mockPatchRedoRowId = vi.fn() const mockPatchUndoRowId = vi.fn() const mockClear = vi.fn() +const mockPruneLayoutActions = vi.fn() const storeState = { stacks: {}, @@ -50,6 +51,7 @@ const storeState = { patchRedoRowId: mockPatchRedoRowId, patchUndoRowId: mockPatchUndoRowId, clear: mockClear, + pruneLayoutActions: mockPruneLayoutActions, } vi.mock('@/stores/table/store', () => ({ diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index 5f29f847d10..c3d44938ff7 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -110,6 +110,13 @@ interface UseTableUndoProps { * reorder was stored rather than silently rewriting the "All" layout. */ onPersistLayout?: (patch: TableMetadata) => void + /** + * Active view id (`null` for "All" or when views are disabled). Layout is + * view-owned, so recorded layout actions are stamped with it and dropped when + * the user switches away — otherwise an undo would write the outgoing view's + * column order into whichever view happens to be active at the time. + */ + activeViewId?: string | null } export function useTableUndo({ @@ -123,6 +130,7 @@ export function useTableUndo({ getPinnedColumns, getColumnWidths, onPersistLayout, + activeViewId = null, }: UseTableUndoProps) { const push = useTableUndoStore((s) => s.push) const popUndo = useTableUndoStore((s) => s.popUndo) @@ -130,6 +138,7 @@ export function useTableUndo({ const patchRedoRowId = useTableUndoStore((s) => s.patchRedoRowId) const patchUndoRowId = useTableUndoStore((s) => s.patchUndoRowId) const clear = useTableUndoStore((s) => s.clear) + const pruneLayoutActions = useTableUndoStore((s) => s.pruneLayoutActions) const canUndo = useTableUndoStore((s) => (s.stacks[tableId]?.undo.length ?? 0) > 0) const canRedo = useTableUndoStore((s) => (s.stacks[tableId]?.redo.length ?? 0) > 0) @@ -161,14 +170,20 @@ export function useTableUndo({ getColumnWidthsRef.current = getColumnWidths const getLocksRef = useRef(getLocks) getLocksRef.current = getLocks + const activeViewIdRef = useRef(activeViewId) + activeViewIdRef.current = activeViewId useEffect(() => { return () => clear(tableId) }, [clear, tableId]) + useEffect(() => { + pruneLayoutActions(tableId, activeViewId) + }, [pruneLayoutActions, tableId, activeViewId]) + const pushUndo = useCallback( (action: TableUndoAction) => { - push(tableId, action) + push(tableId, action, activeViewIdRef.current) }, [push, tableId] ) diff --git a/apps/sim/stores/table/store.test.ts b/apps/sim/stores/table/store.test.ts new file mode 100644 index 00000000000..cc7f7f3d28b --- /dev/null +++ b/apps/sim/stores/table/store.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { useTableUndoStore } from '@/stores/table/store' +import type { TableUndoAction } from '@/stores/table/types' + +const TABLE = 'tbl-1' + +const reorder: TableUndoAction = { + type: 'reorder-columns', + previousOrder: ['a', 'b'], + newOrder: ['b', 'a'], +} +const updateCell: TableUndoAction = { + type: 'update-cell', + rowId: 'r1', + columnName: 'a', + previousValue: 1, + newValue: 2, +} + +describe('pruneLayoutActions', () => { + beforeEach(() => { + useTableUndoStore.getState().clear(TABLE) + }) + + it('drops a layout action recorded under a different view', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, reorder, 'view-a') + + store.pruneLayoutActions(TABLE, 'view-b') + + expect(useTableUndoStore.getState().stacks[TABLE]?.undo).toHaveLength(0) + }) + + it('keeps a layout action when the owning view is still active', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, reorder, 'view-a') + + store.pruneLayoutActions(TABLE, 'view-a') + + expect(useTableUndoStore.getState().stacks[TABLE]?.undo).toHaveLength(1) + }) + + it('keeps row actions across a view switch — rows are table-scoped', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, updateCell, 'view-a') + + store.pruneLayoutActions(TABLE, 'view-b') + + const undo = useTableUndoStore.getState().stacks[TABLE]?.undo + expect(undo).toHaveLength(1) + expect(undo?.[0].action.type).toBe('update-cell') + }) + + it('prunes the redo stack too, so redo cannot replay into the wrong view', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, reorder, 'view-a') + store.popUndo(TABLE) + expect(useTableUndoStore.getState().stacks[TABLE]?.redo).toHaveLength(1) + + store.pruneLayoutActions(TABLE, 'view-b') + + expect(useTableUndoStore.getState().stacks[TABLE]?.redo).toHaveLength(0) + }) + + it('treats All (null) as its own owner', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, reorder, null) + + store.pruneLayoutActions(TABLE, 'view-a') + expect(useTableUndoStore.getState().stacks[TABLE]?.undo).toHaveLength(0) + }) +}) diff --git a/apps/sim/stores/table/store.ts b/apps/sim/stores/table/store.ts index fc4df2808c7..011d3ed5c05 100644 --- a/apps/sim/stores/table/store.ts +++ b/apps/sim/stores/table/store.ts @@ -7,6 +7,7 @@ import { generateShortId } from '@sim/utils/id' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import type { TableUndoAction, TableUndoStacks, TableUndoState, UndoEntry } from './types' +import { LAYOUT_UNDO_ACTIONS } from './types' const STACK_CAPACITY = 100 const EMPTY_STACKS: TableUndoStacks = { undo: [], redo: [] } @@ -99,10 +100,15 @@ export const useTableUndoStore = create()( (set, get) => ({ stacks: {}, - push: (tableId: string, action: TableUndoAction) => { + push: (tableId: string, action: TableUndoAction, viewId: string | null) => { if (undoRedoInProgress) return - const entry: UndoEntry = { id: generateShortId(), action, timestamp: Date.now() } + const entry: UndoEntry = { + id: generateShortId(), + action, + timestamp: Date.now(), + viewId, + } set((state) => { const current = state.stacks[tableId] ?? EMPTY_STACKS @@ -182,6 +188,17 @@ export const useTableUndoStore = create()( }) }, + pruneLayoutActions: (tableId: string, viewId: string | null) => { + const current = get().stacks[tableId] + if (!current) return + const owned = (entry: UndoEntry) => + !LAYOUT_UNDO_ACTIONS.has(entry.action.type) || entry.viewId === viewId + const undo = current.undo.filter(owned) + const redo = current.redo.filter(owned) + if (undo.length === current.undo.length && redo.length === current.redo.length) return + set((state) => ({ stacks: { ...state.stacks, [tableId]: { undo, redo } } })) + }, + clear: (tableId: string) => { set((state) => { const { [tableId]: _, ...rest } = state.stacks diff --git a/apps/sim/stores/table/types.ts b/apps/sim/stores/table/types.ts index 444f41bf10d..e4b4a92fb1e 100644 --- a/apps/sim/stores/table/types.ts +++ b/apps/sim/stores/table/types.ts @@ -87,8 +87,25 @@ export interface UndoEntry { id: string action: TableUndoAction timestamp: number + /** + * Active view when the action was recorded — `null` for "All" or when views + * are disabled. Layout is view-owned, so a layout action is only meaningful + * against the view that owned it; see {@link LAYOUT_UNDO_ACTIONS}. + */ + viewId: string | null } +/** + * Action types whose undo writes column layout (order, widths, pinning). These + * are scoped to the view that was active when they were recorded; every other + * action type operates on rows or the schema and is table-scoped. + */ +export const LAYOUT_UNDO_ACTIONS = new Set([ + 'create-column', + 'delete-column', + 'reorder-columns', +]) + export interface TableUndoStacks { undo: UndoEntry[] redo: UndoEntry[] @@ -96,10 +113,15 @@ export interface TableUndoStacks { export interface TableUndoState { stacks: Record - push: (tableId: string, action: TableUndoAction) => void + push: (tableId: string, action: TableUndoAction, viewId: string | null) => void popUndo: (tableId: string) => UndoEntry | null popRedo: (tableId: string) => UndoEntry | null patchRedoRowId: (tableId: string, oldRowId: string, newRowId: string) => void patchUndoRowId: (tableId: string, oldRowId: string, newRowId: string) => void clear: (tableId: string) => void + /** + * Drops layout actions recorded under a different view. Called on every view + * switch so undo can never write one view's layout into another. + */ + pruneLayoutActions: (tableId: string, viewId: string | null) => void } From d8c279bf719c40003974798e2de5dd9071c19fa4 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 15:26:25 -0700 Subject: [PATCH 23/33] fix(tables): send only the layout keys an action changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin, reorder, and insert-column each shipped a full columnWidths snapshot they never modified. Both sinks merge at top-level key granularity, so an earlier-issued patch landing after a concurrent resize replaced the newer width map with its stale copy. Resize, auto-resize, and delete-column still send columnWidths — those genuinely change it. --- .../[tableId]/components/table-grid/table-grid.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 e8316fd7950..ec3524f890e 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 @@ -712,10 +712,12 @@ export function TableGrid({ setColumnOrder(newOrder) columnOrderRef.current = newOrder } + // Only the keys this action changes. Both sinks merge, and a patch carrying + // an unchanged `columnWidths` snapshot would clobber a concurrent resize + // whose write happened to land first. updateMetadataRef.current({ pinnedColumns: newPinned, ...(orderChanged ? { columnOrder: newOrder } : {}), - columnWidths: columnWidthsRef.current, }) }, []) @@ -1776,10 +1778,7 @@ export function TableGrid({ newOrder: finalOrder, }) setColumnOrder(finalOrder) - updateMetadataRef.current({ - columnWidths: columnWidthsRef.current, - columnOrder: finalOrder, - }) + updateMetadataRef.current({ columnOrder: finalOrder }) } } setDragColumnName(null) @@ -3199,10 +3198,7 @@ export function TableGrid({ const insertIdx = anchorIdx + (side === 'right' ? 1 : 0) newOrder.splice(insertIdx, 0, newColumn) setColumnOrder(newOrder) - updateMetadataRef.current({ - columnWidths: columnWidthsRef.current, - columnOrder: newOrder, - }) + updateMetadataRef.current({ columnOrder: newOrder }) }, [] ) From 429d0e2f7eb9dda3bb56444f187f902141ea6b5f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 15:39:27 -0700 Subject: [PATCH 24/33] fix(tables): don't strand layout writes while views load, keep schema undos across views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more instances of layout being written without a known owner. The sink was left unbound until a view resolved, so a resize/reorder/pin (or the column-append effect) during the views fetch fell through to the table's shared metadata — corrupting All for a table about to adopt a view, and losing the edit to the re-seed. The sink is now bound while the query is in flight and suppresses the write. An error counts as settled, so a failed views fetch falls back to All instead of suppressing layout writes for the session. Pruning was also too broad: create-column and delete-column are table-scoped schema ops that merely have a layout side-effect, so dropping them on a view switch made a deleted column unrecoverable. Only reorder-columns is purely layout and still prunes; the other two survive and have just their layout half suppressed at replay when the recorded view isn't active. --- .../[workspaceId]/tables/[tableId]/table.tsx | 27 ++++++++++++++++--- apps/sim/hooks/use-table-undo.ts | 21 ++++++++++----- apps/sim/stores/table/store.test.ts | 25 +++++++++++++++++ apps/sim/stores/table/store.ts | 4 +-- apps/sim/stores/table/types.ts | 22 +++++++-------- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 083bbcb4ee5..a4c84bdc819 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -358,7 +358,11 @@ export function Table({ ((previousName: string, newName: string) => void) | null >(null) - const { data: views = NO_VIEWS, isSuccess: viewsLoaded } = useTableViews({ + const { + data: views = NO_VIEWS, + isSuccess: viewsLoaded, + isError: viewsFailed, + } = useTableViews({ workspaceId, tableId, enabled: viewsEnabled, @@ -389,6 +393,12 @@ export function Table({ * rendering an empty view. */ const activeView = activeViewId ? (views.find((view) => view.id === activeViewId) ?? null) : null + /** The views query hasn't settled, so which view owns the layout isn't known + * yet and layout writes must go nowhere. An ERROR counts as settled: the table + * falls back to All and writes resume against shared metadata, rather than + * staying silently suppressed for the rest of the session. */ + const viewOwnerUnknown = viewsEnabled && !viewsLoaded && !viewsFailed + const [viewModal, setViewModal] = useState(null) /** Which view id the local filter/sort/hidden state was last seeded from. * `undefined` means "nothing seeded yet" so the first resolve still runs. */ @@ -643,8 +653,10 @@ export function Table({ /** Column order/width/pinning auto-saves into the active view as the user drags, * which is why `isSameViewConfig` excludes layout from the dirty check. Sent as * a `configPatch` so the server merges it — two overlapping layout writes must - * not each replace the whole blob from their own snapshot. With no view active - * the grid keeps writing the table's shared metadata. */ + * not each replace the whole blob from their own snapshot. With All selected + * the sink is unbound and the grid writes the table's shared metadata instead; + * while the views query is still loading the sink IS bound and the write is + * suppressed, because the owner isn't known yet. */ const handlePersistLayout = useCallback( (patch: TableMetadata) => { liveLayoutRef.current = { ...liveLayoutRef.current, ...patch } @@ -1340,7 +1352,14 @@ export function Table({ hiddenColumns={effectiveHiddenColumns} viewLayout={activeView?.config ?? null} viewLayoutKey={activeView?.id ?? null} - onPersistLayout={activeView ? handlePersistLayout : undefined} + onPersistLayout={ + // While the views query is in flight the layout owner is unknown, so + // the sink is bound anyway: `handlePersistLayout` buffers into + // `liveLayoutRef` and suppresses the write. Leaving it unset would fall + // through to the table's shared metadata and corrupt All's layout for a + // table that is about to adopt a view. + viewOwnerUnknown || activeView ? handlePersistLayout : undefined + } columnRenameSinkRef={columnRenameSinkRef} afterDeleteRowsSinkRef={afterDeleteRowsSinkRef} afterDeleteAllSinkRef={afterDeleteAllSinkRef} diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index c3d44938ff7..6fd2e74b47c 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -189,7 +189,12 @@ export function useTableUndo({ ) const executeAction = useCallback( - async (action: TableUndoAction, direction: 'undo' | 'redo') => { + async (action: TableUndoAction, direction: 'undo' | 'redo', entryViewId: string | null) => { + // Column create/delete are table-scoped, so they stay undoable after a view + // switch — but the layout they recorded belongs to the view that was active + // at the time. Replaying it elsewhere would write one view's order/widths + // into another, so the schema half runs and the layout half is dropped. + const entryOwnsLayout = entryViewId === activeViewIdRef.current try { switch (action.type) { case 'update-cell': { @@ -338,7 +343,7 @@ export function useTableUndo({ metadata.pinnedColumns = newPinned } if (Object.keys(metadata).length > 0) { - persistLayoutRef.current(metadata) + if (entryOwnsLayout) persistLayoutRef.current(metadata) } }, }) @@ -430,7 +435,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - persistLayoutRef.current(metadata) + if (entryOwnsLayout) persistLayoutRef.current(metadata) } }, } @@ -459,7 +464,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - persistLayoutRef.current(metadata) + if (entryOwnsLayout) persistLayoutRef.current(metadata) } }, }) @@ -521,6 +526,10 @@ export function useTableUndo({ ...restored.filter((n) => !pinnedSet.has(n)), ] } + // Pruning already drops these on a view switch, so a mismatch here + // should be unreachable; the guard keeps the invariant local rather + // than dependent on when the prune effect happens to run. + if (!entryOwnsLayout) break onColumnOrderChangeRef.current?.(order) persistLayoutRef.current({ columnOrder: order }) break @@ -546,7 +555,7 @@ export function useTableUndo({ } const entry = popUndo(tableId) if (!entry) return - void runWithoutRecording(() => executeAction(entry.action, 'undo')) + void runWithoutRecording(() => executeAction(entry.action, 'undo', entry.viewId)) }, [popUndo, tableId, executeAction]) const redo = useCallback(() => { @@ -560,7 +569,7 @@ export function useTableUndo({ } const entry = popRedo(tableId) if (!entry) return - void runWithoutRecording(() => executeAction(entry.action, 'redo')) + void runWithoutRecording(() => executeAction(entry.action, 'redo', entry.viewId)) }, [popRedo, tableId, executeAction]) return { pushUndo, undo, redo, canUndo, canRedo } diff --git a/apps/sim/stores/table/store.test.ts b/apps/sim/stores/table/store.test.ts index cc7f7f3d28b..e1acbe309ea 100644 --- a/apps/sim/stores/table/store.test.ts +++ b/apps/sim/stores/table/store.test.ts @@ -12,6 +12,18 @@ const reorder: TableUndoAction = { previousOrder: ['a', 'b'], newOrder: ['b', 'a'], } +const deleteColumn: TableUndoAction = { + type: 'delete-column', + columnName: 'a', + columnType: 'string', + columnPosition: 0, + columnUnique: false, + columnRequired: false, + cellData: [], + previousOrder: ['a', 'b'], + previousWidth: null, + previousPinnedColumns: null, +} const updateCell: TableUndoAction = { type: 'update-cell', rowId: 'r1', @@ -65,6 +77,19 @@ describe('pruneLayoutActions', () => { expect(useTableUndoStore.getState().stacks[TABLE]?.redo).toHaveLength(0) }) + it('keeps column create/delete across a view switch — they are schema ops', () => { + const store = useTableUndoStore.getState() + store.push(TABLE, deleteColumn, 'view-a') + + store.pruneLayoutActions(TABLE, 'view-b') + + const undo = useTableUndoStore.getState().stacks[TABLE]?.undo + expect(undo).toHaveLength(1) + expect(undo?.[0].action.type).toBe('delete-column') + // Its recorded owner survives so the replay can drop just the layout half. + expect(undo?.[0].viewId).toBe('view-a') + }) + it('treats All (null) as its own owner', () => { const store = useTableUndoStore.getState() store.push(TABLE, reorder, null) diff --git a/apps/sim/stores/table/store.ts b/apps/sim/stores/table/store.ts index 011d3ed5c05..7072c030073 100644 --- a/apps/sim/stores/table/store.ts +++ b/apps/sim/stores/table/store.ts @@ -7,7 +7,7 @@ import { generateShortId } from '@sim/utils/id' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import type { TableUndoAction, TableUndoStacks, TableUndoState, UndoEntry } from './types' -import { LAYOUT_UNDO_ACTIONS } from './types' +import { VIEW_SCOPED_UNDO_ACTIONS } from './types' const STACK_CAPACITY = 100 const EMPTY_STACKS: TableUndoStacks = { undo: [], redo: [] } @@ -192,7 +192,7 @@ export const useTableUndoStore = create()( const current = get().stacks[tableId] if (!current) return const owned = (entry: UndoEntry) => - !LAYOUT_UNDO_ACTIONS.has(entry.action.type) || entry.viewId === viewId + !VIEW_SCOPED_UNDO_ACTIONS.has(entry.action.type) || entry.viewId === viewId const undo = current.undo.filter(owned) const redo = current.redo.filter(owned) if (undo.length === current.undo.length && redo.length === current.redo.length) return diff --git a/apps/sim/stores/table/types.ts b/apps/sim/stores/table/types.ts index e4b4a92fb1e..030377b775e 100644 --- a/apps/sim/stores/table/types.ts +++ b/apps/sim/stores/table/types.ts @@ -90,21 +90,21 @@ export interface UndoEntry { /** * Active view when the action was recorded — `null` for "All" or when views * are disabled. Layout is view-owned, so a layout action is only meaningful - * against the view that owned it; see {@link LAYOUT_UNDO_ACTIONS}. + * against the view that owned it; see {@link VIEW_SCOPED_UNDO_ACTIONS}. */ viewId: string | null } /** - * Action types whose undo writes column layout (order, widths, pinning). These - * are scoped to the view that was active when they were recorded; every other - * action type operates on rows or the schema and is table-scoped. + * Action types that do NOTHING but rearrange columns, so they mean nothing + * outside the view that recorded them and are dropped on a view switch. + * + * Deliberately excludes `create-column`/`delete-column`: those are table-scoped + * schema operations that merely have a layout side-effect, so they stay + * undoable everywhere. Their layout half is suppressed at replay time instead — + * see `entryOwnsLayout` in `use-table-undo`. */ -export const LAYOUT_UNDO_ACTIONS = new Set([ - 'create-column', - 'delete-column', - 'reorder-columns', -]) +export const VIEW_SCOPED_UNDO_ACTIONS = new Set(['reorder-columns']) export interface TableUndoStacks { undo: UndoEntry[] @@ -120,8 +120,8 @@ export interface TableUndoState { patchUndoRowId: (tableId: string, oldRowId: string, newRowId: string) => void clear: (tableId: string) => void /** - * Drops layout actions recorded under a different view. Called on every view - * switch so undo can never write one view's layout into another. + * Drops purely-layout actions recorded under a different view. Called on every + * view switch so undo can never write one view's layout into another. */ pruneLayoutActions: (tableId: string, viewId: string | null) => void } From d5a12f1a5b6f44af62ed29727d21bfe31471dca5 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 16:14:03 -0700 Subject: [PATCH 25/33] fix(tables): flush layout buffered during load when the owner settles to All Suppressing the write while the views query was in flight stopped All from being corrupted, but nothing resolved the buffer afterwards, so a resize during load looked applied and vanished on refresh. Settling on All re-seeds nothing (viewLayoutKey never changed), so the gesture is still on screen and is now persisted to shared metadata. Adopting a view re-seeds the grid from that view and has already replaced the gesture on screen, so the buffer is dropped to match. --- .../[workspaceId]/tables/[tableId]/table.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index a4c84bdc819..f18ea6599b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -44,6 +44,7 @@ import { useRenameTable, useRunColumn, useTableViews, + useUpdateTableMetadata, useUpdateTableView, } from '@/hooks/queries/tables' import { useInlineRename } from '@/hooks/use-inline-rename' @@ -386,6 +387,7 @@ export function Table({ }) const createViewMutation = useCreateTableView({ workspaceId, tableId }) const updateViewMutation = useUpdateTableView({ workspaceId, tableId }) + const updateMetadataMutation = useUpdateTableMetadata({ workspaceId, tableId }) const deleteViewMutation = useDeleteTableView({ workspaceId, tableId }) /** The selected view, or `null` for the built-in "All" state. A view id that no @@ -587,6 +589,28 @@ export function Table({ liveLayoutRef.current = {} }, [activeView?.id]) + /** Layout captured before the views query settled, when no owner was known. */ + const pendingLayoutRef = useRef({}) + + /** + * Resolves that buffer once the owner is known. + * + * Settling on All re-seeds nothing — `viewLayoutKey` never changed — so the + * user's resize is still on screen and has to be persisted or it silently + * disappears on refresh. Adopting a view instead re-seeds the grid from that + * view's config, which already replaced the gesture on screen, so the buffer is + * dropped to match what the user is looking at. + */ + useEffect(() => { + if (viewOwnerUnknown) return + const pending = pendingLayoutRef.current + pendingLayoutRef.current = {} + if (activeView || !userPermissions.canEdit) return + if (Object.keys(pending).length === 0) return + updateMetadataMutation.mutate(pending) + // `.mutate` is stable in TanStack Query v5, so it stays out of the deps. + }, [viewOwnerUnknown, activeView, userPermissions.canEdit]) + const currentViewConfig = useMemo( () => ({ // `liveLayoutRef` after the cached config for the same reason the blank-view @@ -663,13 +687,19 @@ export function Table({ // The resize grip and drag handles stay live for read-only members, so // without this a resize fires a write-gated PATCH and an error toast. Local // layout still updates — only the persist is suppressed. - if (!activeView || !userPermissions.canEdit) return + if (!userPermissions.canEdit) return + // Owner not known yet — hold the patch until the views query settles. + if (viewOwnerUnknown) { + pendingLayoutRef.current = { ...pendingLayoutRef.current, ...patch } + return + } + if (!activeView) return updateViewMutation.mutate( { viewId: activeView.id, configPatch: patch }, { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) } ) }, - [activeView, userPermissions.canEdit] + [activeView, userPermissions.canEdit, viewOwnerUnknown] ) const handleSaveView = () => { From b614085cabd42bde2fb8dc782eb5768fa200729e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 16:34:59 -0700 Subject: [PATCH 26/33] fix(tables): resolve undo layout ownership at write time, not dispatch time The layout writes for column create/delete happen in mutation success callbacks, and persistLayoutRef is rebound every render. Resolving entryOwnsLayout up front meant the guard could still hold from before a view switch while the sink it guarded already pointed at the destination, writing the recorded view's layout into whichever view was now active. Now a function, so the guard and the sink are read at the same moment: switch away mid-mutation and the schema half still lands while the layout half is dropped, matching the rule that undo only ever affects the view on screen. --- apps/sim/hooks/use-table-undo.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index 6fd2e74b47c..ae60fe0e02b 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -194,7 +194,12 @@ export function useTableUndo({ // switch — but the layout they recorded belongs to the view that was active // at the time. Replaying it elsewhere would write one view's order/widths // into another, so the schema half runs and the layout half is dropped. - const entryOwnsLayout = entryViewId === activeViewIdRef.current + // + // Evaluated at CALL time, not here: these writes happen in mutation success + // callbacks, and `persistLayoutRef` is rebound on every render. A guard + // resolved up front would still hold from before a switch while the sink it + // guards already pointed at the destination view. + const entryOwnsLayout = () => entryViewId === activeViewIdRef.current try { switch (action.type) { case 'update-cell': { @@ -343,7 +348,7 @@ export function useTableUndo({ metadata.pinnedColumns = newPinned } if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout) persistLayoutRef.current(metadata) + if (entryOwnsLayout()) persistLayoutRef.current(metadata) } }, }) @@ -435,7 +440,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout) persistLayoutRef.current(metadata) + if (entryOwnsLayout()) persistLayoutRef.current(metadata) } }, } @@ -464,7 +469,7 @@ export function useTableUndo({ } } if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout) persistLayoutRef.current(metadata) + if (entryOwnsLayout()) persistLayoutRef.current(metadata) } }, }) @@ -529,7 +534,7 @@ export function useTableUndo({ // Pruning already drops these on a view switch, so a mismatch here // should be unreachable; the guard keeps the invariant local rather // than dependent on when the prune effect happens to run. - if (!entryOwnsLayout) break + if (!entryOwnsLayout()) break onColumnOrderChangeRef.current?.(order) persistLayoutRef.current({ columnOrder: order }) break From e2acf9ee5011da637930171b609e44e38f813e35 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 17:47:49 -0700 Subject: [PATCH 27/33] fix(tables): read layout from the grid instead of mirroring it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapper kept two shadow copies of the grid's column layout — liveLayoutRef and pendingLayoutRef — and all three of this round's findings were that mirror going stale: - liveLayoutRef was only cleared on activeView.id change, so widths buffered before the views query settled survived into All, where layout writes bypass the mirror entirely. Both create paths spread it last, so a saved view stored those snapped-back widths. - currentViewConfig memoized a spread of that ref, and mutating a ref doesn't re-run a memo, so Save as view sent the pre-gesture layout. - The flush effect keyed on activeView, but adoption writes the view id through the URL, so for one render the query had settled while activeView was still null — flushing to All in exactly the case that had to drop. The grid owns this state, so it now publishes a reader through a sink ref and the wrapper asks at the moment it needs a value. Nothing to keep in sync, so nothing to go stale. Only whether an unowned change happened is tracked, and the resolve effect — which is what actually picks the owner — decides to persist or drop. --- .../components/table-grid/table-grid.tsx | 15 +++ .../[workspaceId]/tables/[tableId]/table.tsx | 99 +++++++++---------- 2 files changed, 64 insertions(+), 50 deletions(-) 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 ec3524f890e..e8e7f2dfda7 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 @@ -247,6 +247,12 @@ interface TableGridProps { * `columnWidths` / `columnOrder` keys). The wrapper just forwards the call. */ columnRenameSinkRef: React.MutableRefObject<((oldName: string, newName: string) => void) | null> + /** + * Ref the grid populates with a reader for its CURRENT column layout. The grid + * owns this state, so the wrapper asks for it when creating a view rather than + * mirroring every patch — a mirror goes stale the moment a write bypasses it. + */ + layoutSnapshotSinkRef?: React.MutableRefObject<(() => TableMetadata) | null> /** * Ref the grid populates with its post-row-delete cleanup (push undo, * clear selection). The wrapper invokes after the row-delete modal's @@ -383,6 +389,7 @@ export function TableGrid({ viewLayoutKey = null, onPersistLayout, columnRenameSinkRef, + layoutSnapshotSinkRef, afterDeleteRowsSinkRef, afterDeleteAllSinkRef, confirmDeleteColumnsSinkRef, @@ -666,6 +673,14 @@ export function TableGrid({ // the grid. Reads through refs, so identity stability isn't required. columnRenameSinkRef.current = handleColumnRename + if (layoutSnapshotSinkRef) { + layoutSnapshotSinkRef.current = () => ({ + columnWidths: columnWidthsRef.current, + ...(columnOrderRef.current ? { columnOrder: columnOrderRef.current } : {}), + pinnedColumns: pinnedColumnsRef.current, + }) + } + function getColumnWidths() { return columnWidthsRef.current } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index f18ea6599b6..1dcae53c541 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -439,6 +439,42 @@ export function Table({ [setTableParams] ) + /** Reader for the grid's CURRENT column layout, populated by the grid itself. + * The grid owns widths/order/pinning, so the wrapper asks at the moment it + * needs them instead of mirroring every patch — a mirror only stays right + * while every write flows through it, and layout writes bypass it whenever + * All is active. */ + const layoutSnapshotRef = useRef<(() => TableMetadata) | null>(null) + const readLayout = useCallback((): TableMetadata => layoutSnapshotRef.current?.() ?? {}, []) + + /** Whether the user changed layout before the views query settled, when there + * was no owner to write to. What changed isn't recorded — the grid still holds + * it — only that something did, so a settle to All knows to persist it. */ + const layoutTouchedWhileUnownedRef = useRef(false) + + /** + * Resolves that pending layout once the resolve effect has picked an owner. + * + * Settling on All re-seeds nothing — `viewLayoutKey` never changed — so the + * user's resize is still on screen and has to be persisted or it silently + * disappears on refresh. Adopting a view instead re-seeds the grid from that + * view's config, which already replaced the gesture on screen, so it is dropped. + * + * Called from the resolve effect rather than keyed on `activeView`: adoption + * writes the view id through the URL, so for one render the query has settled + * while `activeView` is still null, and an effect would flush to All in exactly + * the case that must drop. + */ + const resolvePendingLayout = useCallback( + (adoptedView: boolean) => { + if (!layoutTouchedWhileUnownedRef.current) return + layoutTouchedWhileUnownedRef.current = false + if (adoptedView || !userPermissions.canEdit) return + updateMetadataMutation.mutate(readLayout()) + }, + [userPermissions.canEdit, readLayout] + ) + /** What the user has already set by hand, for the first-resolve `keep`. */ const localWork = () => ({ sort: sortColumn !== null, @@ -481,6 +517,7 @@ export function Table({ seededViewIdRef.current = defaultView.id setTableParams({ view: defaultView.id }) applyViewConfig(defaultView.config, keep) + resolvePendingLayout(true) return } // No view to adopt. Deliberately does NOT apply an empty config — that @@ -488,16 +525,19 @@ export function Table({ // exception: nothing about them refers to this table, so they're cleared. seededViewIdRef.current = null if (inheritedParams) setTableParams({ view: ALL_VIEW_PARAM, sort: null, dir: null }) + resolvePendingLayout(false) return } if (activeViewId === ALL_VIEW_PARAM) { seededViewIdRef.current = null + resolvePendingLayout(false) return } // A `?view=` that resolves to nothing (deleted view, stale bookmark) falls // back to "All" without touching state, for the same reason. An explicit // `?sort=` alongside `?view=` also wins over the view's stored sort. seededViewIdRef.current = activeView?.id ?? null + resolvePendingLayout(activeView !== null) if (activeView) { applyViewConfig(activeView.config, localWork()) } else { @@ -541,6 +581,7 @@ export function Table({ sortColumn, applyViewConfig, setTableParams, + resolvePendingLayout, ]) /** @@ -576,48 +617,9 @@ export function Table({ * order / pins the grid is rendering (they live in the table's shared metadata * until a view owns them) instead of creating a layout-less view that then * resets the grid. Updates never send this — they send a merge patch. */ - /** Last layout the grid reported, patch by patch. View layout writes have no - * optimistic cache update, so `activeView.config` lags an in-flight resize — - * this is what a new view copies so the grid can't snap back. - * - * Declared above the consumers that read it during render, and reset on every - * view change: it holds patches for ONE view, so carrying them across a switch - * would let a new view inherit the previous view's widths/order/pins. */ - const liveLayoutRef = useRef({}) - - useEffect(() => { - liveLayoutRef.current = {} - }, [activeView?.id]) - - /** Layout captured before the views query settled, when no owner was known. */ - const pendingLayoutRef = useRef({}) - - /** - * Resolves that buffer once the owner is known. - * - * Settling on All re-seeds nothing — `viewLayoutKey` never changed — so the - * user's resize is still on screen and has to be persisted or it silently - * disappears on refresh. Adopting a view instead re-seeds the grid from that - * view's config, which already replaced the gesture on screen, so the buffer is - * dropped to match what the user is looking at. - */ - useEffect(() => { - if (viewOwnerUnknown) return - const pending = pendingLayoutRef.current - pendingLayoutRef.current = {} - if (activeView || !userPermissions.canEdit) return - if (Object.keys(pending).length === 0) return - updateMetadataMutation.mutate(pending) - // `.mutate` is stable in TanStack Query v5, so it stays out of the deps. - }, [viewOwnerUnknown, activeView, userPermissions.canEdit]) - const currentViewConfig = useMemo( () => ({ - // `liveLayoutRef` after the cached config for the same reason the blank-view - // payload does it: a resize/reorder/pin still in flight would otherwise be - // captured at its pre-drag value and snap back when the view is selected. ...(activeView?.config ?? tableData?.metadata), - ...liveLayoutRef.current, filter: effectiveFilter, sort: sortQuery, hiddenColumns: effectiveHiddenColumns, @@ -683,14 +685,15 @@ export function Table({ * suppressed, because the owner isn't known yet. */ const handlePersistLayout = useCallback( (patch: TableMetadata) => { - liveLayoutRef.current = { ...liveLayoutRef.current, ...patch } // The resize grip and drag handles stay live for read-only members, so // without this a resize fires a write-gated PATCH and an error toast. Local // layout still updates — only the persist is suppressed. if (!userPermissions.canEdit) return - // Owner not known yet — hold the patch until the views query settles. + // Owner not known yet. The grid keeps the layout either way, so only the + // fact of the change is recorded; `resolvePendingLayout` reads the live + // value once an owner exists. if (viewOwnerUnknown) { - pendingLayoutRef.current = { ...pendingLayoutRef.current, ...patch } + layoutTouchedWhileUnownedRef.current = true return } if (!activeView) return @@ -741,18 +744,13 @@ export function Table({ const blank = viewModal?.blank === true const config: TableViewConfig = blank ? { - // `liveLayoutRef` last, so a resize/reorder/pin still in flight wins over - // the cached copy — otherwise the new view stores the pre-drag layout and - // the grid snaps back to it on selection. (With no active view the grid - // writes table metadata directly, which `useUpdateTableMetadata` updates - // optimistically, so that branch is already current.) ...(activeView?.config ?? tableData?.metadata), - ...liveLayoutRef.current, + ...readLayout(), filter: null, sort: null, hiddenColumns: [], } - : currentViewConfig + : { ...currentViewConfig, ...readLayout() } createViewMutation.mutate( { name, config }, { @@ -1385,12 +1383,13 @@ export function Table({ onPersistLayout={ // While the views query is in flight the layout owner is unknown, so // the sink is bound anyway: `handlePersistLayout` buffers into - // `liveLayoutRef` and suppresses the write. Leaving it unset would fall + // records the change and suppresses the write. Leaving it unset would fall // through to the table's shared metadata and corrupt All's layout for a // table that is about to adopt a view. viewOwnerUnknown || activeView ? handlePersistLayout : undefined } columnRenameSinkRef={columnRenameSinkRef} + layoutSnapshotSinkRef={layoutSnapshotRef} afterDeleteRowsSinkRef={afterDeleteRowsSinkRef} afterDeleteAllSinkRef={afterDeleteAllSinkRef} confirmDeleteColumnsSinkRef={confirmDeleteColumnsSinkRef} From be51268b3b9ec754d42bcab419906d6e49b1b6db Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 18:16:55 -0700 Subject: [PATCH 28/33] fix(tables): flush unowned layout when the views fetch fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolve effect is the only caller of resolvePendingLayout and gated on isSuccess, which never becomes true on a query error — so layout touched during the load window was never persisted on the error path, even though the table had already settled to All and later writes worked. The error branch now flushes to shared metadata, matching the rest of the error path's fall-back-to-All behavior. --- .../[workspaceId]/tables/[tableId]/table.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 1dcae53c541..c86be1daf38 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -492,7 +492,16 @@ export function Table({ * view even after someone changes which view is default. */ useEffect(() => { - if (!viewsEnabled || !viewsLoaded) return + if (!viewsEnabled) return + // A failed fetch settles to All (`viewOwnerUnknown` cleared, sink unbound), + // so layout touched during the load must flush here — no other branch of + // this effect ever runs on the error path, and skipping it would silently + // drop the resize on refresh. + if (viewsFailed) { + resolvePendingLayout(false) + return + } + if (!viewsLoaded) return if (seededViewIdRef.current === undefined) { // Embedded tables bind these parsers to the HOST page's URL, which the @@ -574,6 +583,7 @@ export function Table({ }, [ viewsEnabled, viewsLoaded, + viewsFailed, views, activeView, activeViewId, From b622f7867db578d8497d649a4380774e68a1ff5b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 18:25:55 -0700 Subject: [PATCH 29/33] fix(tables): gate the on-screen layout restore by ownership, not just the persist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The undo success callbacks applied the recorded view's order/widths/pinning to the grid unconditionally and only gated the PATCH, so switching views before a column create/delete mutation resolved left the destination displaying the origin view's layout until switched away and back. Each callback now checks ownership where its layout work begins: three are purely layout and return at the top; delete-column undo restores cell data first (row data, runs everywhere) and gates only the layout block below it. In a non-owning view the restored column still appears via the grid's append effect — at the end, leaving that view's layout untouched. --- apps/sim/hooks/use-table-undo.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index ae60fe0e02b..c7c24254838 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -334,6 +334,12 @@ export function useTableUndo({ if (direction === 'undo') { deleteColumnMutation.mutate(colKey, { onSuccess: () => { + // Everything below is layout cleanup for the recorded view. In + // any other view the grid shows that view's own layout — the + // schema change has already landed and dangling width/pin keys + // are pruned on read, so touching the screen OR persisting here + // would push the origin view's layout onto the one on display. + if (!entryOwnsLayout()) return const metadata: TableMetadata = {} const currentWidths = getColumnWidthsRef.current?.() ?? {} if (colKey in currentWidths) { @@ -347,9 +353,7 @@ export function useTableUndo({ onPinnedColumnsChangeRef.current?.(newPinned) metadata.pinnedColumns = newPinned } - if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout()) persistLayoutRef.current(metadata) - } + if (Object.keys(metadata).length > 0) persistLayoutRef.current(metadata) }, }) } else { @@ -409,6 +413,11 @@ export function useTableUndo({ } })() } + // Cell restore above runs everywhere — it's row data. The + // layout below belongs to the recorded view: elsewhere the + // restored column still reappears via the grid's append + // effect, but at the end, leaving the on-screen layout alone. + if (!entryOwnsLayout()) return const metadata: TableMetadata = {} if (action.previousOrder) { onColumnOrderChangeRef.current?.(action.previousOrder) @@ -439,15 +448,14 @@ export function useTableUndo({ } } } - if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout()) persistLayoutRef.current(metadata) - } + if (Object.keys(metadata).length > 0) persistLayoutRef.current(metadata) }, } ) } else { deleteColumnMutation.mutate(colKey, { onSuccess: () => { + if (!entryOwnsLayout()) return const metadata: TableMetadata = {} if (action.previousOrder) { const newOrder = action.previousOrder.filter((n) => n !== colKey) @@ -468,9 +476,7 @@ export function useTableUndo({ metadata.pinnedColumns = newPinned } } - if (Object.keys(metadata).length > 0) { - if (entryOwnsLayout()) persistLayoutRef.current(metadata) - } + if (Object.keys(metadata).length > 0) persistLayoutRef.current(metadata) }, }) } From 23dc262caa0283f33e0f3543e8af066dcf476dbc Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 18:37:29 -0700 Subject: [PATCH 30/33] fix(tables): route all layout writes through one owner-aware sink, reconcile order at seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two holes closed: The sink binding toggled on activeView, leaving a render-frame gap after the views query settled but before the resolve effect adopted a default — writes in that gap fell through to shared metadata. The sink is now always bound while views are enabled and handlePersistLayout is the single router, reading the owner at call time: unresolved buffers, a view patches the view, All writes metadata. The error branch stamps the owner so post-error writes stop buffering. The append effect only fires when the schema changes, so a column that arrived while another source owned the layout rendered via the displayColumns fallback but was never written into the adopted owner's stored order until a drag happened to heal it. Seeding now reconciles the incoming order against the schema and persists the appended tail through the current sink. --- .../components/table-grid/table-grid.tsx | 18 +++++++- .../[workspaceId]/tables/[tableId]/table.tsx | 45 ++++++++++--------- 2 files changed, 41 insertions(+), 22 deletions(-) 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 e8e7f2dfda7..8751d986f79 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 @@ -1898,7 +1898,23 @@ export function TableGrid({ metadataSeededRef.current = true seededLayoutKeyRef.current = viewLayoutKey setColumnWidths(source?.columnWidths ?? {}) - setColumnOrder(source?.columnOrder ?? null) + // Reconcile the incoming order against the schema before seeding it. The + // append effect only fires when `columns` changes, so a column that arrived + // while another source owned the layout (or none did, during load) would + // otherwise render via the `displayColumns` fallback but never be written + // into this owner's stored order until the next drag happened to heal it. + const seededOrder = source?.columnOrder ?? null + if (seededOrder) { + const known = new Set(seededOrder) + const missing = schemaColumnsRef.current.map(getColumnId).filter((id) => !known.has(id)) + const reconciled = missing.length > 0 ? [...seededOrder, ...missing] : seededOrder + setColumnOrder(reconciled) + if (missing.length > 0 && canEditRef.current) { + updateMetadataRef.current({ columnOrder: reconciled }) + } + } else { + setColumnOrder(null) + } setPinnedColumns(source?.pinnedColumns ?? []) return } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index c86be1daf38..bec735e5e04 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -493,11 +493,12 @@ export function Table({ */ useEffect(() => { if (!viewsEnabled) return - // A failed fetch settles to All (`viewOwnerUnknown` cleared, sink unbound), - // so layout touched during the load must flush here — no other branch of - // this effect ever runs on the error path, and skipping it would silently - // drop the resize on refresh. + // A failed fetch settles to All: stamp the owner so the layout router stops + // buffering and writes shared metadata, then flush what was touched during + // the load — no other branch of this effect ever runs on the error path, + // and skipping it would silently drop the resize on refresh. if (viewsFailed) { + seededViewIdRef.current = null resolvePendingLayout(false) return } @@ -699,18 +700,24 @@ export function Table({ // without this a resize fires a write-gated PATCH and an error toast. Local // layout still updates — only the persist is suppressed. if (!userPermissions.canEdit) return - // Owner not known yet. The grid keeps the layout either way, so only the - // fact of the change is recorded; `resolvePendingLayout` reads the live - // value once an owner exists. - if (viewOwnerUnknown) { + // Owner not decided yet — either the query is in flight, or it settled + // this frame and the resolve effect hasn't picked a view (adoption writes + // the id through the URL, so `activeView` lags a render). The grid keeps + // the layout either way, so only the fact of the change is recorded; + // `resolvePendingLayout` reads the live value once an owner exists. + if (viewOwnerUnknown || seededViewIdRef.current === undefined) { layoutTouchedWhileUnownedRef.current = true return } - if (!activeView) return - updateViewMutation.mutate( - { viewId: activeView.id, configPatch: patch }, - { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) } - ) + if (activeView) { + updateViewMutation.mutate( + { viewId: activeView.id, configPatch: patch }, + { onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) } + ) + return + } + // All: the shared table metadata is the owner. + updateMetadataMutation.mutate(patch) }, [activeView, userPermissions.canEdit, viewOwnerUnknown] ) @@ -1390,14 +1397,10 @@ export function Table({ hiddenColumns={effectiveHiddenColumns} viewLayout={activeView?.config ?? null} viewLayoutKey={activeView?.id ?? null} - onPersistLayout={ - // While the views query is in flight the layout owner is unknown, so - // the sink is bound anyway: `handlePersistLayout` buffers into - // records the change and suppresses the write. Leaving it unset would fall - // through to the table's shared metadata and corrupt All's layout for a - // table that is about to adopt a view. - viewOwnerUnknown || activeView ? handlePersistLayout : undefined - } + // Always bound while views are enabled: the router reads the owner at + // call time (buffer / view / All-metadata), so no binding gap can send a + // write to the wrong place between settle and adoption. + onPersistLayout={viewsEnabled ? handlePersistLayout : undefined} columnRenameSinkRef={columnRenameSinkRef} layoutSnapshotSinkRef={layoutSnapshotRef} afterDeleteRowsSinkRef={afterDeleteRowsSinkRef} From 1576c41ef7356b0eed6cd09029a3edbe5764776b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 18:49:45 -0700 Subject: [PATCH 31/33] fix(tables): capture the layout owner when a schema action is dispatched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert-column and the delete chain persist layout from mutation callbacks through updateMetadataRef, which always targets the current sink — so a view switch mid-flight wrote the origin view's order/widths/pins into the destination. Undo got this guard already; the live paths never did. Both now capture viewLayoutKey at dispatch and compare at the callback. On a mismatch the layout work is skipped: the destination re-seeded its own layout, the new column lands there via the append effect when the refetch arrives, and the deleted column's dangling keys are pruned on read. pushUndo also takes the captured owner as an override — stamped at callback time it would record the destination, letting a later undo apply the origin's layout to it. --- .../components/table-grid/table-grid.tsx | 86 ++++++++++++------- apps/sim/hooks/use-table-undo.ts | 9 +- 2 files changed, 61 insertions(+), 34 deletions(-) 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 8751d986f79..147ba3f8b76 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 @@ -445,6 +445,12 @@ export function TableGrid({ const [pinnedColumns, setPinnedColumns] = useState([]) const pinnedColumnsRef = useRef(pinnedColumns) pinnedColumnsRef.current = pinnedColumns + /** Current layout owner (view id, or null for All) for capture-at-dispatch + * guards: a mutation callback persisting layout must compare the owner it was + * dispatched under against this, or a mid-flight view switch writes the + * origin's layout into the destination — same rule as undo's entryOwnsLayout. */ + const viewLayoutKeyRef = useRef(viewLayoutKey) + viewLayoutKeyRef.current = viewLayoutKey const metadataSeededRef = useRef(false) /** Which layout source the grid last seeded from, so a view switch re-seeds. */ const seededLayoutKeyRef = useRef(null) @@ -3239,18 +3245,20 @@ export function TableGrid({ const index = schemaColumnsRef.current.findIndex((c) => getColumnId(c) === columnId) if (index === -1) return const name = generateColumnName() + const owner = viewLayoutKeyRef.current addColumnMutation.mutate( { name, type: 'string', position: index }, { onSuccess: (result) => { const newId = result.data.columns.find((c) => c.name === name)?.id ?? name - pushUndoRef.current({ - type: 'create-column', - columnName: name, - columnId: newId, - position: index, - }) - insertColumnInOrder(columnId, newId, 'left') + pushUndoRef.current( + { type: 'create-column', columnName: name, columnId: newId, position: index }, + owner + ) + // Skipped after a mid-flight view switch: the destination re-seeded + // its own order, and the append effect places the new column there + // when the schema refetch lands. + if (owner === viewLayoutKeyRef.current) insertColumnInOrder(columnId, newId, 'left') }, } ) @@ -3264,18 +3272,17 @@ export function TableGrid({ if (index === -1) return const name = generateColumnName() const position = index + 1 + const owner = viewLayoutKeyRef.current addColumnMutation.mutate( { name, type: 'string', position }, { onSuccess: (result) => { const newId = result.data.columns.find((c) => c.name === name)?.id ?? name - pushUndoRef.current({ - type: 'create-column', - columnName: name, - columnId: newId, - position, - }) - insertColumnInOrder(columnId, newId, 'right') + pushUndoRef.current( + { type: 'create-column', columnName: name, columnId: newId, position }, + owner + ) + if (owner === viewLayoutKeyRef.current) insertColumnInOrder(columnId, newId, 'right') }, } ) @@ -3421,6 +3428,9 @@ export function TableGrid({ originalPositions.set(name, { position: def ? cols.indexOf(def) : cols.length, def }) } const deletedOriginalPositions: number[] = [] + // Layout owner when the chain was dispatched — every onDeleted below fires + // from a mutation callback, so it must not trust the sink current by then. + const owner = viewLayoutKeyRef.current const deleteNext = (index: number) => { if (index >= columnsToDelete.length) return @@ -3438,24 +3448,36 @@ export function TableGrid({ const onDeleted = () => { deletedOriginalPositions.push(entry.position) - pushUndoRef.current({ - type: 'delete-column', - // `columnToDelete` is the stable id; record the display name for re-create. - columnName: entry.def?.name ?? columnToDelete, - columnId: columnToDelete, - columnType: entry.def?.type ?? 'string', - columnPosition: adjustedPosition >= 0 ? adjustedPosition : cols.length, - columnUnique: entry.def?.unique ?? false, - columnRequired: entry.def?.required ?? false, - // Without these a deleted select column can't be re-created — it is - // invalid with no options, and the saved cell data is option ids. - ...(entry.def?.options ? { columnOptions: entry.def.options } : {}), - ...(entry.def?.multiple ? { columnMultiple: true } : {}), - cellData, - previousOrder: orderSnapshot, - previousWidth, - previousPinnedColumns: pinnedSnapshot, - }) + pushUndoRef.current( + { + type: 'delete-column', + // `columnToDelete` is the stable id; record the display name for re-create. + columnName: entry.def?.name ?? columnToDelete, + columnId: columnToDelete, + columnType: entry.def?.type ?? 'string', + columnPosition: adjustedPosition >= 0 ? adjustedPosition : cols.length, + columnUnique: entry.def?.unique ?? false, + columnRequired: entry.def?.required ?? false, + // Without these a deleted select column can't be re-created — it is + // invalid with no options, and the saved cell data is option ids. + ...(entry.def?.options ? { columnOptions: entry.def.options } : {}), + ...(entry.def?.multiple ? { columnMultiple: true } : {}), + cellData, + previousOrder: orderSnapshot, + previousWidth, + previousPinnedColumns: pinnedSnapshot, + }, + owner + ) + + // The cleanup below edits the ORIGIN view's layout. After a mid-flight + // switch the grid has re-seeded to the destination; dangling keys for + // the deleted column there are pruned on read, so skip rather than push + // origin snapshots into the destination's state or its stored config. + if (owner !== viewLayoutKeyRef.current) { + deleteNext(index + 1) + return + } const { [columnToDelete]: _removedWidth, ...cleanedWidths } = columnWidthsRef.current setColumnWidths(cleanedWidths) diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index c7c24254838..c1d4c72c9d6 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -182,8 +182,13 @@ export function useTableUndo({ }, [pruneLayoutActions, tableId, activeViewId]) const pushUndo = useCallback( - (action: TableUndoAction) => { - push(tableId, action, activeViewIdRef.current) + /** + * `owner` overrides the recorded view for actions pushed from a mutation + * callback — by then the active view may have changed, and stamping the + * destination would let a later undo apply the origin's layout to it. + */ + (action: TableUndoAction, owner?: string | null) => { + push(tableId, action, owner === undefined ? activeViewIdRef.current : owner) }, [push, tableId] ) From b49a061e268f1599ca6a4d3475d22f26e7cf7f80 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 28 Jul 2026 18:58:25 -0700 Subject: [PATCH 32/33] fix(tables): resolve views on list availability, not query success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed background refetch flips isError while the cached list stays usable, and every view mutation invalidates the views query — so one blip made the resolve effect treat views as terminally failed and stop applying switches until the next successful refetch. The axis is now whether a list exists: error with no list ever fetched settles to All; error with a cached list resolves normally against the cache; owner is unknown only while the initial fetch is in flight. --- .../[workspaceId]/tables/[tableId]/table.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index bec735e5e04..98b9e218295 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -359,15 +359,17 @@ export function Table({ ((previousName: string, newName: string) => void) | null >(null) - const { - data: views = NO_VIEWS, - isSuccess: viewsLoaded, - isError: viewsFailed, - } = useTableViews({ + const { data: viewsData, isError: viewsErrored } = useTableViews({ workspaceId, tableId, enabled: viewsEnabled, }) + const views = viewsData ?? NO_VIEWS + /** A views list exists — fresh or cached. A failed background refetch flips + * `isError` while the cached list stays perfectly usable (and every view + * mutation invalidates this query), so success/error is the wrong axis: + * what matters is whether there is a list to resolve against. */ + const viewsAvailable = viewsData !== undefined // Single source of truth for `useTable` — drives both the grid render and // the wrapper's slideouts/modals. The grid receives the bundle as props. @@ -399,7 +401,7 @@ export function Table({ * yet and layout writes must go nowhere. An ERROR counts as settled: the table * falls back to All and writes resume against shared metadata, rather than * staying silently suppressed for the rest of the session. */ - const viewOwnerUnknown = viewsEnabled && !viewsLoaded && !viewsFailed + const viewOwnerUnknown = viewsEnabled && !viewsAvailable && !viewsErrored const [viewModal, setViewModal] = useState(null) /** Which view id the local filter/sort/hidden state was last seeded from. @@ -493,16 +495,18 @@ export function Table({ */ useEffect(() => { if (!viewsEnabled) return - // A failed fetch settles to All: stamp the owner so the layout router stops - // buffering and writes shared metadata, then flush what was touched during - // the load — no other branch of this effect ever runs on the error path, - // and skipping it would silently drop the resize on refresh. - if (viewsFailed) { + // Terminal only when the fetch failed WITHOUT ever producing a list — then + // the table settles to All: stamp the owner so the layout router stops + // buffering, and flush what was touched during the load. An error with a + // cached list falls through — the list is still resolvable, and treating a + // failed background refetch as terminal would wedge view switching until + // the next successful refetch. + if (viewsErrored && !viewsAvailable) { seededViewIdRef.current = null resolvePendingLayout(false) return } - if (!viewsLoaded) return + if (!viewsAvailable) return if (seededViewIdRef.current === undefined) { // Embedded tables bind these parsers to the HOST page's URL, which the @@ -583,8 +587,8 @@ export function Table({ applyViewConfig(activeView?.config ?? null) }, [ viewsEnabled, - viewsLoaded, - viewsFailed, + viewsAvailable, + viewsErrored, views, activeView, activeViewId, From 14c2681c3ea08c9adb214c50f52665b2b0a772eb Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 29 Jul 2026 11:34:56 -0700 Subject: [PATCH 33/33] ci: raise Build App timeout to 25 minutes The build outgrew the 15-minute cap over the last two days of merges: 10m02 (c6acc62a07), then 14m44 after the folders/desktop/library batch (adc557a2f2, 16s under the limit), then two consecutive timeouts on 7b1af4121b after the outlook merge. Staging's own latest runs show the same signature (one cancelled). GitHub labels a job timeout "cancelled", which is why these read as cancellations. --- .github/workflows/test-build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 195d19dcd38..c50e076a317 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -222,7 +222,11 @@ jobs: build: name: Build App runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'linux-x64-8-core' }} - timeout-minutes: 15 + # Build durations crossed 15 minutes as the app grew (10m02 on Jul 29 AM, + # 14m44 after the folders/desktop/library merges, then two straight + # timeouts) — GitHub reports a job timeout as "cancelled". 25 keeps + # headroom without masking a genuine hang. + timeout-minutes: 25 steps: - name: Checkout code